接上文叙述,阅读《CLR via C#》总结(第二天)中理解了局部变量,实参,形参在运行时于调用栈的相互关系,那么接下来探究一下类型对象与堆之间的交互关系。
定义如下两个内部类:
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() {...}
}
我们在这里定义一个M3方法,以此方法中的运行来观察
void M3()
{
Employee e;
Int32 year;
e = new Manager();
e = Employee.LookUp("Joe");
year = e.GetYearsEmployed();
e.GetProgressReport();
}
在调用M3方法前,已经加载到进程中,堆已经初始化,线程栈已创建,马上调用M3方法。
JIT编译器会先确认该方法引用到的类型(Employee,Manager,Int32,String) 的所有程序集都已加载,然后利用程序集的元数据,CLR提取与这些类型有关的信息创建数据结构来表示类型本身。因string和Int32非常常用,假定都已创建,我们只对Employee和Manager类型对象进行讨论。
前面说过,堆上的所有对象都包含两个额外成员:类型对象指针和同步块索引,类型内部有静态数据字段,此外还包含一个放发表,每个方法都有对应的记录项。
构造manager对象时,CLR会自动初始化“类型对象指针“指向该对象引用的类型对象,并在调用构造方法之前初始化同步块索引,并将对象的所有字段初始化为0或null。
new操作符返回Manager对象的内存地址,该内存地址变量保存到对应声明的对象e上(e是在线程栈上的)。
接下来执行到e=Employee.Lookup("Joe");查找一个对象,并将对象地址返回到e变量上,调用静态方法(最上方有代码定义)时,CLR会定位与定义类型对应的类型对象,JIT编译器查找对应的记录项,对方法进行编译(如果需要的话),最后调用编译好的代码进行查找,找到后会在堆上定义一个新的Manager对象,用Joe的信息初始化它并将此对象的地址返回到变量e上。
可以看到e此时不再引用前一个Manager对象,GC会自动将他回收。
接下来执行e的GetYearsEmployed方法,此方法是非虚实例方法,JIT会找到e的定义类型为Employee,e此时会定义成它的定义类型Employee并查找Employee类型对象中是否有定义GetYearsEmployed方法,如果没有会回溯类的层次结构(即查找它的父类,能回溯的原因是因为上面e引用的是一个manager类型对象)。将获取的值返回给栈上的year变量上。
最后执行e.GetProgressReport方法,调用虚方法时,我们会发现Manager与Employee类都有这个方法,则此时调用是看上面对e的赋值,如果lookup方法当时返回的是manager对象则调用Manager类型方法,否则调用Employee类的方法。
最后考虑一下Employee和Manager类型对象在最初创建的时候是如何初始化的?
Employee和Manager这类自己定义的类型对象在一个进程运行时会立即视为MSCorLib.dll中的System.Type类创建的特殊对象,他们的类型对象指针会初始化成对System.Type类型对象的引用。
另外System.Type也是对象所以它的类型对象指针会指向它自己。
由此,我们可知,System.Object中的GetType方法返回存储在对象的类型对象指针成员中的地址,可以判断任何对象的真实类型。