当调用某一个类型的 static function ,或是 某一个 isntance 的 virtual function, non-virtual function 时,
CLR 与 JIT 是如何在 stack 与 heap 之间,找到并执行相关的 function 内容呢?
前言
标题有点鸟,不过主要就是要讲 CLR via C# (我看的是第三版,最新版本已经出到第四版了)书中 chapter 4: Type Fundametals 中,有一段解释 runtime 时,调用 virtual function, non-virtual function, static function 等不同类型的 function 时,在 2 个 class 有继承关系时,其 thread stack 与 heap 的变化。
这一段是整本书我最喜欢的段落之一,也算的上是启蒙我对 type 认知最重要的一段说明。
这一篇文章,就借花献佛一下,摘要说明一下下列几点的影响:
- 声明变量的类型
- 执行个体 ( instance ) 的类型
- 调用 instance function
- 调用 non-virtual function
- 调用 virtual function 与 override function
- 调用 static function
说明 memory 的图示,都是源自于 CLR via C# 一书。
阅读此章节,请务必先行理解继承、多态、virtual、override、static 、heap、stack 的基本定义。
类定义
首先有两个 class ,分别为 Employee 与 Manager。其类关系为 Manager 继承 Employee ,如下图所示:
两个类的程序如下所示:
internal class Employee
{
public Int32 GetYearsEmployed()
{
return 5;
}
public virtual String GetProgressReport()
{
return "Employee's GetProgressReport";
}
public static Employee Lookup(String name)
{
return new Manager { Name = name };
}
}
internal class Manager : Employee
{
public string Name { get; set; }
public override string GetProgressReport()
{
return "Manager overrides GetProgressReport";
}
}
Employee 上有几个重点:
- non-virtual function:
GetyearsEmployed()
,代表子类无法覆写。 - virtual function:
GetProgressReport()
,代表子类可能覆写。 - static function:
Lookup()
,代表这个 function 是专属于 Employee 这个 type 的 function ,另外一个要注意的是,回传的类型是 Employee ,也就代表回传的执行个体,可以是 Employee 的子类(例如范例中的 Manager)
Manager 上的重点:
- 继承自 Employee
- 覆写
GetProgressReport()
: 按照多态的概念,当调用声明为 Employee 变量的GetProgressReport()
方法时,会依照该变量的 instance 类型为 Employee 或 Manager,来决定要调用哪一个类的方法内容。
接下来的说明,有兴趣阅读原文的朋友,可以参考书中 chapter 4 的 “How Things Relate at Runtime”,这边只是用我的理解来做说明。
Runtime Memory 使用说明
先来看一下 context 的程序:
void M3(string[] args)
{
Employee e;
Int32 year;
e = new Manager();
e = Employee.Lookup("Joe");
year = e.GetYearsEmployed();
e.GetProgressReport();
}
接下来,我们将说明每一行程序,其 memory 的使用情况。
Heap 刚建立,Thread Stack for M3 Function
Figure 4-6 ( from CLR via C# )
一开始, thread stack 跟 heap 都是空的,接着要来执行 M3()
这个 function 了。
准备调用 M3() ,Employee 与 Manager 的 Type Object 被初始化
Figure 4-7 ( from CLR via C# )
当 JIT 要将 M3()
方法转成 IL 时,会发现里面有使用到 Manager 与 Employee 这两个 type (当然还包括了 Int32 与 String 两个 type ,但这不是这边的重点,就不多做说明),接着 JIT 会确认参考到的 assemblies 有没有 load 进来这些 type 。
所以,这两个 type object 会被载入 heap 中。
每一个 type object 都会有 type object pointer 与 sync block index 两个 members ,如果该 type 有定义 static fields 的话,也会随着 type object 被载入到 heap 中。以下简单说明一下这几个东西:
- Type object pointer: 用来指向这个 instance ( 别忘了 type object 也是一种 instance ,它的 type 为 System.Type ) 的 type 位址
- Sync block index: 在 multi-thread 中用来控制同步的东西 (简单的说,可以让 multi-thread 透过它排队)
- Static field: 跟着 type object 的生命周期,因为每一个 type object 只会有一份,所以 static 可以拿来做 singleton, process 的全域变量等应用,相对的也要小心重入 (re-entry) 造成的问题。
- Method table: 定义这个 type object 中,拥有哪一些 method ,可以看到 Manager 的 type object 上,只有定义了
GenProgressReport
这个 override method ,而 Employee type object 则有定义 3 个 method 。
当 CLR 确定 M3()
要使用到的 type object 都已经准备好后,M3()
也已经经过 JIT 编译后, CLR 允许 thread 开始执行 M3 的 native code 。
开始执行 M3() ,初始化区域变量默认值
Figure 4-8 ( from CLR via C# )
首先,先看到 M3()
中的第一行与第二行,声明了 2 个区域变量,分别为 e 与 year , CLR 会自动给这些区域变量默认值为 null 或 0 (reference type 为 null, value type 为 0)。
这时,还只有 stack 上有配置这 2 个区域变量, heap 还没被 reference 或被 M3()
使用到。
初始化对象的变化: e = new Manager();
Figure 4-9 ( from CLR via C# )
接着透过 new
operator 来调用 Manager 的构造函数,此时会回传 Manager object (执行个体)的位址,并存放在 e 的变量中,也就是在 stack 中, e 的内容是存放刚刚初始化的 manager object 在 heap 中的内存位址。
而就程序看起来,就只是把一个被初始化的 manager instance assign 给 e 这个变量。
简单记法: reference type 是在 stack 上存位址, value type 则是在 stack 上存内容
因此,可以看到 stack 上, e 的内容即关联到 heap 上,刚刚初始化完成的那个 manager object 。而因为这个 instance 的类型是 Manager ,所以这个 manager object 的 type object pointer 会指到 manager type object 的位置。
调用静态方法: e = Employee.Lookup("Joe");
Figure 4-10 ( from CLR via C# )
接着,调用 Employee 上的静态方法: Lookup(String name)
,并将回传结果 assign 给 e 变化,这样会产生什么变化呢?
- 调用静态方法时, JIT compiler 会先找到这个静态方法的 type object ,然后从 type object 的 method table 中找到这个方法,将此方法内容即时编译(如果之前还没经过 JIT 编译过,才需要即时编译,若已经编译过,会有记录),执行编译后的内容。
- 这边的内容是初始化一个 manager 对象,将 Name 属性设为 Joe ,接着回传这个 Name 为 Joe 的 manager object 的位址,塞给 stack 中的 e 。
这时可以发现,原本上一行在 M3()
中初始化的 manager object 没有其他地方参考到它了,但是它仍会存在一段时间,等待 gc 起来之后,再依据 gc 的算法来回收这一个 heap 的内存。
到这边,e 这个区域变量,已经指到透过 Employee.Lookup("Joe")
所回传在 heap 中的 manager object 位址。
调用 non-virtual function: year = e.GetYearsEmployed();
Figure 4-11 ( from CLR via C# )
当调用 e.GetYearsEmployed()
时,此方法并未被声明为 virtual (这边称为 non-virtual),也就代表不会有子类去覆写这个方法,所以 JIT compiler 会先找到这个变量的声明类型,也就是 e 的类型,在这边为 Employee 。
接着寻找 Employee 的 type object 中,是否存在着 GetYearsEmployed()
这个方法,若不存在,则 JIT complier 会一路往其父类找,默认最终会找到 Object 。
原因很简单,虽然变量类型声明为 Employee ,但是调用 e 这个执行个体的方法,这个方法可能是因为继承链上的父类拥有, Employee 上才能被调用。(例如任何类默认都继承 Object ,所以任何执行个体默认都可以调用 Object 的方法,如 ToString() ,若继承链上都没有其他 ToString() 的方法,那么最终 JIT compiler 就会调用 Object type object 上 method table 中的 ToString 方法。)
JIT compiler 能一路往父类寻找,是因为每一个 type object 上,有一个 field 指向 base type (图上没有标示),因此可以一路找到最原始的 Object 。
找到这个 non-virtual 的执行个体方法后,一样,若有需要 JIT compiler 会即时编译这个方法内容,然后执行 JIT 之后的程序。
以这例子来说,GetYearsEmployed()
会回传 5 ,5 是 int ,是 value type ,因此 5 这个值会被 assign 到 stack 上 year 的内容中。
调用 virtual function: e.GenProgressReport();
Figure 4-12 ( from CLR via C# )
接着调用 Employee 上定义的 virtual function: GenProgressReport()
,当调用的是 virtual function 时,此时 JIT compiler 会额外产生一些 code 来处理,处理什么呢?
JIT compiler 会找到该变量 e 这个 instance 的执行个体类型为何,也就是透过在 stack 上 e 所存放的位址,找到了在 heap 中的执行个体,并透过 type object pointer 找到了 manager type object ,这个时候会寻找 manager type object 上的 method table ,确定是否有 GenProgressReport
这个方法,在这个例子中,因为 Manager 有 override GenProgressReport()
,因此 JIT compiler 找到后,会用同样的方式来执行 JIT 后的程序。
要注意的是,倘若 Employee.Lookup()
所回传的执行个体类型,若为 Employee 时,这边 JIT compiler 会找到的就应该是 Employee type object 上的 GenProgressReport
方法,而非 Manager type object 的方法。
书上虽没有提到,若 Manager 没有 override GenProgressReport()
的情况。不过我想,若 Manager type object 的 method table 找不到方法时, JIT 会用 non-virtual 的方式,往 base type 的 type object 一路寻找,直到 Object 的 type object 。
简单的说,也就是声明为 virtual 所影响的,是 JIT 会先找到 instance 的 type object,以此为起点,寻找 type object 或继承链上 type object 的 method table 。这样要找到实际的方法内容,会比 non-virtual 花的功夫更多。因为 non-virtual 是直接从声明的类型开始找,不必考虑 instance 的类型是否为子类的类型。
补充:Type Object 的 type object pointer 指到哪?
Figure 4-13 ( from CLR via C# )
大家已经知道一般的 object 其 type object pointer ,就是指到该 instance 所对应的类型位置。那么, type object 的 type object pointer 又指去哪呢?答案是 System.Type 这个 type object ,也就是这些 type object 的类型,其实都是属于 System.Type 的一种。而最后 Type 的 type object ,其 type object pointer 则是指到自己身上。
说了这么多 type ,大家应该很容易联想到 System.Object 上所定义的 non-virtual instance method: GetType()
吧。
没错!每一个 instance 都可以调用 GetType()
,而这个方法定义在 System.Object 中,其内容就是回传 instance 的 type object pointer 所指到的 type object ,所以其回传类型为 System.Type ,因为 type object 的 type object pointer 指到 System.Type 的 type object 位置。
虽然像绕口令一样,不过了解了 heap 中 object 的关系后,也就没这么难懂了。
结论
CLR via C# 真的是一本不得不看的好书,这一篇文章其实翻译居多,只是鉴于这本书很多读者都因为一开始晦涩难懂而啃不下去,加上英文跟简体中文的内容描述,可能都不是很直觉,所以笔者在这边再反刍一次,也再强调一次,这一个段落真的说明了太多有趣的东西,是我看书之前不知道的,看完真的获益良多。
如果各位读者对于这个段落中的一些基本元素还不是很了解,建议务必要搞清楚,虽然不懂也可以写程序,但打开了这一扇窗,你会看到相当宽广的天空啊。
哪一些元素要知道,简单列出如下:
- stack 与 heap
- static 与 instance
- type 与 instance
- value type 与 reference type
- 继承
- 多态
- virtual 与 non-virtual
- JIT compiler 的角色与功能
我也是从这一个段落才理解了,什么叫做 static ,为什么称为 static ,虽然书中没有明讲,但了解了其 runtime 运行原理,自然会理解 static 这个命名的由来。
或许您会对下列培训课程感兴趣:
- 2019/7/27(六)~2019/7/28(日):演化式设计:测试驱动开发与持续重构 第六梯次(中国台北)
- 2019/8/16(五)~2019/8/18(日):【C#进阶设计-从重构学会高易用性与高弹性API设计】第二梯次(中国台北)
- 2019/9/21(六)~2019/9/22(日):Clean Coder:DI 与 AOP 进阶实战 第二梯次(中国台北)
- 2019/10/19(六):【针对遗留代码加入单元测试的艺术】第七梯次(中国台北)
- 2019/10/20(日):【极速开发】第八梯次(中国台北)
![]() |
想收到第一手公开培训课程资讯,或想询问企业内训、顾问、教练、咨询服务的,请洽 Facebook 粉丝专页:91敏捷开发之路。 |
原文:大专栏 [读书心得]Runtime Type Fundamentals from CLR via C#