运行时的相互关系
首先从一些计算机基础知识开始。
创建线程栈
加载CLR的一个Windows进程,进程中可能有多个线程。线程创建时会分到1MB的栈。
栈空间用于向方法传递实参,方法内部定义的局部变量也在栈上。栈从高位内存地址向地位地址构建。
图中线程已执行了一些代码,栈上已经有一些数据了,现在假定线程执行的代码要调用M1方法。
在方法开始做工作前,还有序幕代码对其进行初始化,在方法做完工作后,还有尾声代码对其进行清理,以便返回调用者。
void M1(){ string name = "Tom"; M2(name); ... return; } void M2(string str){ int length = s.Length; int tally; ... return; }
在方法1中定义局部变量
M1方法开始执行时,他的序幕代码在线程栈上分配局部变量name的内存。
在方法1中调用方法2
然后M1调用M2方法将局部变量name作为实参传递,使name局部变量中的地址被压入栈,M2方法内部使用参数s标识栈位置。
另外,调用方法时还会将返回地址压入栈。被调用的方法在结束后应返回至该位置,如下图所示。
在方法2中定义局部变量
M2方法开始执行,他的序幕代码在线程栈中为局部变量length和tally分配内存。
方法执行结束
然后M2方法内部的代码开始执行,最终方法结束,M2的栈帧 unwind,恢复成之前的样子之后,M1继续执行M2调用之后的代码,M1的栈帧将准确反映M1需要的状态。
栈帧代表当前线程调用栈中的一个方法调用。进行的每个方法调用都会在调用栈中创建并压入一个Stack Frame。
最终,M1会返回到他的调用者,图中未显示,应在name实参上方。M1栈帧 unwind,恢复成之前的样子,之后调用M1的方法继续执行M1调用之后的代码,哪个方法的栈帧将准确反映他需要的状态。
变量、方法和类在线程栈和托管堆的相互关系
现在假设有Sample和Father两个类。
//Father类有实例方法、虚方法和静态方法 internal class Father{ public int NormalMethod(){} public virtual string VirtualMethod(){} public static Father StaticMethod(){} } //Smaple类继承Father类,重写虚方法VirtualMethod internal sealed class Sample : Father{ public override string VirtualMethod(){} }
同时程序的线程执行到了如下方法,方法中定义了如下代码。
void Fuction(){ Father temp; temp = new Sample(); temp = Father.StaticMethod(); int num = temp.NormalMethod(); string str = temp.VirtualMethod(); }
CLR检查方法中引用的所有类型
JIT编译器将方法的IL代码转换成本机CPU指令时,会注意到方法内部引用的所有类型,CLR要确认定义了这些类型的所有程序集都已加载。
①利用程序集的元数据,CLR提取与这些类型有关的的信息创建一些数据结构来表示类型本身。
堆上所有对象都包含两个额外成员:类型对象指针和同步块索引。
定义类型时可在类型内部定义静态数据字段。为这些静态数据字段提供支援的字节在类型对象自身中分配。
每个类型对象最后都包含一个方法表,方法表中类型定义的每个方法都有对应的记录项。
CLR确认方法所需要的所有类型对象是否已经创建,如果创建完毕,等方法代码编译后,线程就开始执行方法的本机代码。
CLR自动将所有局部变量初始化为null或0。然而如果代码试图访问尚未显式初始化的局部变量,C#会报告错误消息:使用了未赋值的局部变量。
②方法中代码构造了一个Sample对象,那么在托管堆就会创建Sample类型的一个实例。
Father temp; temp = new Sample();
和所有对象一样,对象也有类型对象指针和同步块索引。该对象还包含必要的字节类容纳Sample类型定义的所有实例数据字段,以及容纳由Sample的任何基类定义的所有实例字段。
③任何时候在堆上新建对象,CLR都自动初始化内部的类型对象指针成员来引用和对象对应的类型对象。
此外,在调用类型的构造器之前,CLR会先初始化同步块索引,并将对象的所有实例字段设为null或0。new操作符返回Sample对象的内存地址,该地址保存到变量sample中,而sample在线程栈上。
调用静态方法
方法的下一行代码调用Father的静态方法StaticMethod。
temp = Father.StaticMethod();
④调用静态方法时,CLR会定位与静态方法的类型对应的类型对象。
然后JIT编译器在类型对象的方法表中查找与被调用方法对应的记录项,对方法进行JIT编译再调用JIT编译好的代码。
假定Father的StaticMethod方法返回了一个Sample类型的对象,于是StaticMethod方法在堆上构造出一个新的Sample对象,初始化后返回该对象的地址。该地址保存到局部变量temp中。
此时temp不再引用第一个Manager对象,由于没有变量引用该对象,所以他是未来垃圾回收的主要目标。垃圾回收机制将自动回收(释放)该对象占用的内存。
调用非虚实例方法
下一行代码调用Father的非虚实例方法NormalMethod。并把返回结果赋值给int类型变量num。
int num = temp.NormalMethod();
⑤调用非虚实例方法时,JIT编译器会找到与“发出调用的变量temp的类型Father”对应的类型对象即Father类型对象。
这时的变量temp被定义为Father,如果Father没有这个方法,JIT编译器会回溯类层次结构,一直回溯到Object,并在沿途的每个类型中查找该方法。
之所以能这样回溯,是因为每个类型对象都有一个字段引用了它的基类型,这个信息在图中没有显示。
然后,JIT编译器在类型对象的方法表中查找引用了被调用方法的记录项,对方法进行JIT编译,再调用JIT编译好的代码。假定Father的NormalMethod方法返回5,这个操作如下图所示。
调用虚方法
下一行代码调用Father的虚实例方法VirtualMethod。
string str = temp.VirtualMethod();
⑥调用虚实例方法时,JIT编译器要在方法中生成一些额外代码。方法每次调用都会执行这些代码。这些代码首先检查发出调用的变量并跟随地址来到发出调用的对象。
变量temp当前引用的是Sample对象。然后代码在类型对象的方法表中查找引用了被调用方法的记录项,对方法进行JIT编译,再调用JIT编译好的代码。
由于temp引用一个Sample对象,所以会调用Sample的VirtualMethod实现。这个操作如下图所示。
注意,如果Father的StaticMethod方法返回的是Father而不是Sample,StaticMethod会在内部构造一个Father对象,他的类型对象指针将引用Father类型对象。
这样最终执行但的就是Father的StaticMethod实现,而不是Sample的。
Type类型对象
注意Father和Sample类型对象都包含类型对象指针。这是由于类型对象本质上也是对象。CLR创建对象时,必须初始化这些成员。
⑦CLR开始在一个进程运行时,会立即为MSCorLib.dll中定义的System.Type类型创建一个特殊的类型对象。
⑧Father和Sample类型对象都是该类型的“实例”。因此,他们的类型对象指针成员会初始化成对System.Type类型对象的引用,如下图所示。
⑨System.Type类型对象本身也是对象,内部也有类型对象指针成员,这个指针指向它本身。因为System.Type类型对象本身是一个类型对象的“实例”。
⑩System.Object的GetType方法返回存储在指定对象的类型对象指针成员中的地址。也就是说GetType方法返回指向对象的类型对象的指针。这样就可判断系统中任何对象的真实类型。