下图展示了CLR加载一个进程,在这个进程中,可能存在多个线程。当一个线程创建时,它会分配到一个1MB大小的堆栈,这个堆栈空间用于向方法传递实参,并用于存储在方法内部定义的局部变量。堆栈是从高位内存发址向低位内存地址构建的。在图中,线程执行了一些代码,它的堆栈上已有一些数据。

  在一个最基本的方法中,应该包含一些“开场白(prologue)”代码,它们负责在方法开始做它的工作前对其进行初始化。另外,还应包含一些“收场白(epilo-gue)"代码,它们负责在方法完成工作后对其进行清理,以返回调用者。现在,假定线程执行的代码要调用M1方法。当该方法始执行时,它的“开场白”代码从线程的堆栈中为局部变量name分配内存。然后,M1调用M2方法,将局部变量name作为一个实参来传递。这造成name局部变量中的地址被压入堆栈。在M2方法内部,将使用名为s的参数变量来标识堆栈变量。另外,在调用一个方法时,还会将一个“返回地址”压入堆栈。被调用的方法在完成后,应返回到这个位置。

  M2方法开始执行时,它的“开场白”代码从线程的堆栈中为局部变量length和tally分配内存,如下图所示。然后开始执行M2方法内部的代码。最终,M2会抵达它的return语句,这会造成CPU的指令指针被设置成堆栈中的返回地址,而且M2的堆栈帧会进行辗转开解(unwind)。之后,M1将继续执行在M2调用之后的代码,M1的堆栈帧将准确反映M1需要的状态。

  现在,让我们围绕CLR来调整一下讨论。假定有以下两个类定义:

internal class Employee
{
public Int32 GetYearEmployed() {...}
public virtual string GenProgressReport() {...}
public static Employee Lookup(string name) {...}
}

internal sealed class Manager : Employee
{
public override string GetProgressReport() {...}
}

  我们的Windows进程已启动,CLR加裁到其中,托管堆已初始化,且已创建一个线程(连同它的1MB堆栈空间)。该线程已执行了一些代码,现在马上就要调用M3方法。下图展示了目前的状态。

  当JIT编译器将M3的IL代码转换为本地CPU指令时,它会注意到M3内部引用的所有类型:Employee、Int32、Manager及String。这时,CLR要确保定义了这些类型的所有程序集都已加载到AppDomain中。然后,利用程序集的元数据,CLR提取有关这些类型的信息,并创建一些数据结构来表示类型本身。下图展示了用于Employee和Manager类型对象的数据结构。由于这些线程在调用M3前已执行了一些代码,所以假定Int32和String类型对象已创建好了,所以图中没显示它们。

  我们来讨论一下这些类型对象。堆上的所有对象都包含两个额外的成员:类型对象指针和同步块索引。定义一个类型时,可在类型的内部定义静太数据字段。为这些静态数据字段提供支援的字节是在类型对象自身中分配的。在每个类型对象中,最后都包含一个方法表。在方法表中,类型中定义的每个方法都有一个对应的记录项。

  现在,当CLR确定方法需要的所有类型对象都已创建,且M2的代码已经编译后,就允许线程开始执行M3的本地代码。M3的“开场白”代码执行时,必须从线程的堆栈中为局部变量分配内存。另外,作为方法“开场白”代码的一部分,CLR会自动将所有局部变量初始化为null或0。

  然后,M3执行它的代码来构造一个Manager对象,这造成在托管堆中创建Manager类型的一个实例。如下图所示。可以看出,和所有对象一样,Manager对象也有一个类型对象指针和同步块索引。该对象还包含容纳Manager类型定义的所有实例数据字段及其任何基类(本例中就是Employee和Object)定义的所有实例字段所需的字节。任何时候在堆上新建一个对象,CLR都会自动初始化内部类型对象指针成员,让它引用与对象对应的类型对象(本例中就是Manager类型对象)。此外,CLR还会首先初始化同步块索引,并将对象的所有实例设为null或0,然后才会调用类型的构造器(这是有可能修改某些实例数据字段的一个方法)。new操作符会返回Manager对象的内存地址,该地址保存在变量e中(e在线程的堆栈上)。

  M3的下一行代码调用Employee的静态方法Lookup在调用一个静态方法时,CLR会定位与定义静态方法的类型对应的类型对象。然后CLR在类型对象的方法表中定位引用了被调用方法的记录项,然后对方法进行JIT编译(如果需要的话),最后调用JIT编译过的代码。就本例来说,我们假定Employee的Lookup方法要查询一个数据库来查找Joe。在内部,Lookup方法在堆上构造一个新的Manager对象,为Joe初始化它,然后返回该对象的地址。这个地址保存到局部变量e中。如下图所示:

  注意,e不再引用创建的第一个Manager对象。垃圾收集机制会自动回收这个对象的内存。

  M3的下一行代码调用Employee的非虚实例方法GetYearsEmployed。在调用一个非虚实例方法时,CLR会找到与发出调用的变量的类型对象的类型对象。在本例中,变量e被定义成一个Emplyee。然后,CLR在类型对象的方法表找到引用了被调用方法的记录项,对方法进行JIT编译,然后调用JIT编译过的代码。

  M3的下一行代码调用Employee的虚实例方法GenProgressReport。在调用一个虚实方法时,CLR要做一些额外的工作。首先,它在用于发出调用的变量中查找,然后跟随地址到发出调用的对象。本例中,变量e指向Joe Manager对象。然后,CLR检查对象的内部类型对象指针成员,这个成员引用了对象的实例类型。然后,CLR在类型对象的方法表中定位引用了被调用方法的记录项,对方法进行JIT编译,然后调用JIT编译过的代码。本例中,会调用Manager的GenProg-ressReport实现。该操作结果如下图所示:

  注意,Employee和Manager类型对象都包含类型对象指针成员。这是因为类型对象本身实际上也是对象。CLR创建类型时,必须初始化这些成员。那么,具体初始化成什么呢?CLR开始在一个进程中运行时,它会立即为System.Type类型(在MsCorLib.dll中定义)创建一个特殊的类型对象。Employee和Manager类型对象是该类型的“实例”。因此,它们的类型对象指针成员会初始化成对System.Type类型对象的引用。如下图所示:

  当然,System.Type类型对象本身也是一个对象,所以内部也包含一个类型对象指针成员。那么这个指针指向的是什么呢?它指向本身,因为System.Type类型对象本身是一个类型对象的“实例”。

  System.Object的GetType方法返回的是存储在指定对象的“类型对象指针”成员中的地址。换言之,GetType方法返回的是指向对象的类型对象的一个指针。这样一来,就可以判断系统中任何对象(包含类型对象)的真实类型。

posted on 2011-06-19 22:45  辛勤的代码工  阅读(1092)  评论(1编辑  收藏  举报