已加载CLR的一个Windows进程,该进程可能有多个线程。线程创建时会分到1MB的栈。栈空间用于向方法传递实参,方法内部定义的局部变量也在栈上。
以下是方法M1和M2的伪代码
void M1() { String name="Joe"; M2(name); ... return; } void M2(String s) { Int32 length=s.Length; Int32 tally; ... return; }
现在,假定线程执行的代码要调用M1方法。最简单的方法包含序幕(prologue)代码,在方法开始做工作前对其进行初始化。还包含尾声(epilogue)代码,在方法做完工作后对其进行清理,以便返回至调用者。M1方法开始执行时,他的序幕代码在线程栈上分配局部变量name的内存,如下图1所示。
图1
然后M1调用M2方法,将局部变量name作为实参传递。这造成name局部变量中的地址被压入栈。M2方法内部使用参数s标识栈位置。另外,调用方法时还会将返回地址压入栈。被调用的方法在结束后应返回至该位置。如下图2所示。
图2
M2方法开始执行,他的序幕代码在线程栈中为局部变量length和tally分配内存,如下图3所示。然后M2方法内部的代码开始执行,最终M2抵达他的return语句,造成CPU的指令指针被设置为栈中的返回地址,M2的栈帧展开(unwind),恢复成图1的样子.之后,M1继续执行M2调用之后的代码。M1的栈帧将准确反映M1需要的状态。最终,M1会返回到他的调用者,图中未显示,应在name实参上方。
图3
假定有以下两个类定义
internal class Employee { public Int32 GetYearsEmployed(){} public virtual String GetProgressReport(){} public static Employee Lookup(String name){} } internal sealed class Manager:Employee { public override String GetProgressReport(){} }
WIndows进程已启动,CLR已加载到其中,托管堆已初始化,而且已创建一个线程(连同他的1MB栈空间)。线程已执行了一些代码,马上就要调用M3方法,代码如下所示。
void M3() { e=new Manager(); e=Employee.Lookup("Joe"); year=e.GetYearsEmployed(); e.GetProgressReport(); }
堆上所有对象都包含两个额外成员:类型对象指针和同步块索引。如图4所示,Employee和Manager类型对象都有这两个成员。定义类型时,可在类型内部静态数据字段。为这些静态数据字段提供支援的字节在类型对象自身中分配。每个类型对象最后都包含一个方法表。在方法表中,类型定义的每个方法都有对应的记录项。
当CLR确认方法需要的所有类型对象都以创建,M3的代码已经编译之后,就允许线程执行M3的本机代码。M3的序幕代码执行时必须在线程中为局部变量分配类型,如下图4所示。作为方法序幕代码的一部分,CLR自动将所有局部变量初始化为null或0,。然而如果代码试图访问尚未显式初始化的局部变量,C#汇报稿错误消息:使用了未赋值的局部变量。
图4
然后M3执行代码构造一个Manager对象,这造成在托管堆创建Manager类型的一个势力,如下图5所示.可以看出,和所有对象一样,Manager对象也有类型对象指针和同步块索引。该对象还包含必要的字节类容纳Manager类型定义的所有实例数据字段,以及容纳由Manager的任何基类(本例就是Employee和Object)定义的所有实例字段。
任何时候在堆上新建对象,CLR都自动初始化内部的类型对象指针成员来引用和对象对应的类型对象。此外,在调用类型的构造器(本质可能是修改某些实例数据字段的方法)之前,CLR会先初始化同步块索引,并将对象的所有实例字段设为null或0.new操作符返回Manager对象的内存地址,该地址保存到变量e中(e在线程栈上)。
图5
M3的下一行代码调用Employee的静态方法Lookup。调用静态方法时,CLR会定位与静态方法的类型对应的类型对象。然后,JIT编译器在类型对象的方法表中查找与被调用方法对应的记录项,对方法进行JIT编译(如果需要的话)。再调用JIT编译好的代码。
本例假定Employee的Lookup方法要查询数据库来查找Joe,再假定数据库支出Joe是公司的一名经理,所以在内部,Loouo方法在堆上构造出一个新的Manager对象,用Joe的信息初始化它,返回该对象的地址。该地址保存到局部变量e中,这个操作如下图6所示。
注意,e不再引用第一个Manager对象。事实上,由于没有变量引用该对象,所以他是未来垃圾回收的主要目标。垃圾回收机制将自动回收(释放)该对象占用的内存。
图6
M3的下一行代码调用Employee的非虚实例方法GetYearsEmployed。调用非虚实例方法时,JIT编译器会找到与“发出调用的哪个变量e的类型Employee”对应的类型对象即Employee类型对象。这是的变量额被定义成一个Employee,如果Employee类型没有定义正在调用的那个方法,JIT编译器会回溯类层次结构(一直回溯到Object),并在沿途的每个类型中查找该方法。之所以能这样回溯,是因为每个类型对象都有一个字段引用了它的基类型,这个信息在图中没有显示。
然后,JIT编译器在类型对象分方法表中查找引用了被调用方法的记录项,对方法进行JIT编译(如果需要的话),再调用JIT编译好的代码。本例假定Employee的GetYearsEmployed方法返回5,这个操作如下图7所示。
图7
M3的下一行代码调用Employee的虚实例方法GetProgressReport。调用虚实例方法时,JIT编译器要在方法中生成一些额外代码。方法每次调用都会执行这些代码。这些代码首先检查发出调用的变量并跟随地址来到发出调用的对象。变量e当前引用的是代表"Joe"的Manager对象。然后代码在类型对象的方法表中查找引用了被调用方法的记录项,对方法进行JIT编译(如果需要的话),再调用JIT编译好的代码。由于e引用一个Manager对象,所以会调用Manager的GetProgressReport实现。这个操作如下图8所示。
图8
注意,如果Employee的Lookup方法发现Joe是Employee而不是Manager,Lookuo会在内部构造一个Employee对象,他的类型对象指针将引用Employee类型对象。这样最终执行但就是Employee的GetProgressReport实现,而不是Manager的。
注意Employee和Manager类型对象都包含类型对象指针。这是由于类型对象本质上也是对象。CLR创建对象时,必须初始化这些成员。CLR开始在一个进程运行时,会立即为MSCorLib.dll中定义的System.Type类型创建一个特殊的类型对象。Employment和Manager类型对象都是该类型的“实例”。因此,他们的类型对象指针成员会初始化成对System.Type类型对象的引用。如下图9所示。
当然,System.Type类型对象本身也是对象,内部也有类型对象指针成员,这个指针指向它本身。因为System.Type类型对象本身是一个类型对象的“实例”。
System.Object的GetType方法返回存储在指定对象的类型对象指针成员中的地址。也就是说GetType方法返回指向对象的类型对象的指针。这样就可判断系统中任何对象的真实类型。
图9
CLR via C#学习笔记-第四章-类型基础-运行时的相互关系
原文:https://www.cnblogs.com/errornull/p/9741294.html