十:类型、对象、线程堆栈、托管堆栈之间运行时的相互关系(二)
现在看看CLR是怎么工作的,假定有以下两个类定义如下:
internal class Employee
{
public Int32 GetYearsEmployed()
{
//...
}
public virtual String GenProgressReport()
{
//...
}
public static Employee Lookup(String name)
{
//...
}
}
internal sealed class Manager : Employee
{
public override string GenProgressReport()
{
//...
}
}
Windows进程已经启动,CLR已加载到其中,托管堆已经初始化,而且创建一个线程(连同它的1M的堆栈空间),该线程已经执行了一些代码,现在马上要调用M3方法,如下图:
当JIT编译器将M3的IL代码转换成本地CPU指令时,会注意到M3内部引用的所有类型:Employee,Int32,Manager以及String(因为"Joe"),在这个时候,CLR要确保定义了这些类型的所有程序集都已经加载到AppDomain中,然后,利用程序集的元数据,CLR提取有关这些类型的信息,并创建一些数据结构来表示类型本身。下图展示了用于Employee和Manager类型对象我数据结构:
堆上的所有对象都包含两个额外的成员:类型对象指针和同步块索引,可以看出,上面Manager对象和Employee对象都有这两个成员。在定义一个类型时,可以在类型的内部定义静态数据字段,为这些字段提供支援的字节是在类型对象自身中分配的。在每个类型对象中,最后都包含一个方法表,在方法表中,类型中定义的每个方法都有一个对应的记录项,前面已经讨论过这个方法表。如上图,由于Employee类型定义了三个方法,GetYearsEmployed,GetProgressReport,Loolup,所以在Employee的方法表中有三个记录项。由于Manager类型只定义了一个方法,所以它的方法表中只有一个记录项。
当CLR确定方法需要的所有类型对象都已经创建,而且M3的代码经过编译之后,就允许线程开始执行M3的本地代码。M3的“开场白”代码执行时,必须从线程堆中分配内存,CLR会自动将所有的局部变量初始化为null或0,如下图:
然后M3执行它的代码来构造一个Manager对象,这造成在托管堆中创建Manager类型的一个实例,如下图:
和所有对象一样,Manager对象也有一个类型对象指针和同步块索引,该对象还包含容纳Manager类型定义的所有实例数据字段及其基类定义所需的所有实例字段所需的字节。任何时候在堆上新建一个对象,CLR都会自动初始化内部类型对象指针成员,让它引用与对象对应的类型对象(本例是Manager类型对象)。此外,CLR还会首先初始化同步块索引,并将对象的所有实例字段设为null或0,然后才调用类型的构造器,new操作符会返回Manager对象的内存地址,该地址保存在变量e中,在线程堆栈上。
M3的下一行代码调用Employee的静态方法Lookup,在调用一个静态方法时,CLR会定位与定义与静态方法的类型对应的类型对象,然后,CLR在类型对象的方法表中定位引用了调用方法的记录项,然后对方法进行JIT编译(如果需要的话),最后调用JIT编译过的代码。就本例来说,我们假定Employee的Lookup方法要查询一个数据库来查找Joe,另外,假定数据库指出Joe是公司的一名经理,所以在内部,Lookup方法在堆上构造一个新的Manager对象,为Joe初始化它,然后返回对象的地址,这个地址保存到局部变量e中,这个操作结果如下图所示:
注意,e不再引用创建的第一个Manager对象。事实上,由于没有变量引用这个对象,所以它是将来进行垃圾回收的主要候选对象。
M3的下一行代码调用Employee的非虚实例方法GetYearsEmployed,在调用一个非虚实例方法时,CLR会找到与发出调用的变量的类型对应的类型对象。在本例中,变量e被定义成一个Employee(假如Employee类型没有定义正在调用的方法,CLR会回溯类层次结构,在Object中查找该方法),然后,CLR在类型对象的方法表中找到引用了被调用方法的记录项,对方法进行JIT编译(如果需要的话),然后调用JIT编译过的代码,就本例来说,我们假定Employee的GetYearsEmployed方法返回5,如下图所示:
M3的下一行代码调用Employee的虚实例方法GetProgressReport,在调用一个虚实例方法时,CLR要做一些额外的工作,首先,它在用于发出调用的变量中查找,然后跟随地址到发出调用的对象。在本例中,变量e指向Joe Manager对象,然后,CLR检查对象的内部类型对象指定成员,这个成员引用了对象的实际类型。然后,CLR在类型对象的方法表中定位引用了被调用方法的记录项,对方法进行JIT编译(如果需要的话),然后调用JIT编译过的代码,就本例来说,会调用Manager的GetProgressReport实现,因为e引用的是一个Manager对象,这个操作结果如下图:
注意,如果Employee的Lookup方法发现Joe只是一个Employee,而不是一个Manager,Lookup会在内部Employee对象,它的类型对象指针将引用Employee类型对象,这样一来,最终执行的就是Employee的GetProgressReport实现,而不是Manager的GetProgressReport实现。
我们知道对象中包含一个指针,它指向对象的类型对象(类型对象中包含静态字段和方法表)。Employee和Manager类型对象都包含类型对象指针成员,这是因为类型对象本身实际上也是对象。CLR在创建类型对象时,必须初始化这些成员。CLR开始在一个进程中运行时,它会立即人System.Type类型在创建一个特殊的类型对象,Employee和Manager类型对象是该类型的“实例”,因此,它们的类型对象指针成员会初始化成对System.Type类型对象的引用,当然,System.Type类型对象本身也是一个对象,所以内部也包含一个类型对象指针成员,它指向它本身,因为System.Type类型对象本身是一个类型对象的“实例”
,如下图: