阅读目录

  • 前言
  • 线程堆栈的分配
  • 托管堆上对象的分配
  • 结束语

 

前言

   .Net中的运行时,以及各个类型、对象、线程堆栈以及托管堆之间的关系,在初学者(俺是初学者中的菜鸟 J)看来,有很多是难以理解的东西,俺在看了CLR Via C# 的前几章后,现在将文中的大概意思并加以自己的理解,重现运行时,以及各个关系。希望各位尽量拍砖,多多指出不正确的地方,共同进步。

 线程堆栈的分配

 

         图1中展示了CLR加载的一个Microsoft Windows进程。在一个进程中,可能会存在多个线程。在创建一个线程时,这个线程会分配到一个1MB大小的堆栈。这个堆栈空间的作用:用于向方法传递 实参,并用于存储在方法内部定义的局部变量。图1展示了一个线程的堆栈(右侧)。堆栈都是从高位内存向地位内存地址构建。在左侧图中,该线程执行了一些代 码,它的堆栈上已经有一些数据(右图上半部分灰色区域)。现在假定线程要执行M1方法。 

         在一个方法中,应该包含一些开场白代码,负责在方法开始前对变量进行初始化操作,以及一些收场白代码,负责方法执行完毕之后进行清理工作,以便返回调用者。当M1方法开始执行时,它的开场白代码在线程的对战中为局部变量name分配内存,如图2所示: 

接 着,M1中的代码执行,调用M2方法,将局部变量name作为一个实参来传递。这造成name局部变量中的地址被压入堆栈。在M2方法内部,将使用名为s 的形参变量来标识堆栈位置(注意,有的架构通过寄存器来传递实参以提升性能,但这对于当前的讨论来说并不重要)。另外,在调用一个方法时,还会将一个“返 回地址”压入堆栈。以便被调用的方法在完成之后,应该返回到这个位置。参见图3: 

         M2方法开始执行时,他的开场白代码在线程的堆栈中为局部变量length 和tally分配内存,如图4。然后,开始执行M2方法内部的代码。最终,M2会执行到return语句,这时CPU执行指针会被设置成堆栈中刚才存储的[返回地址] ,而 且M2的堆栈帧会进行辗转开解(unwind)(个人大概理解意思是:释放M2的内部局部变量),然后堆栈内部会恢复到图2状态,之后,M1将继续执行后 面代码,最终M1也会返回到它的调用者,这个过程其实跟M2是一样的,M1执行完成之后,M1的堆栈帧会进行辗转开解,恢复成图1所示那样。跟着会执行 M1后续的代码。图4: 

托管堆上对象的分配

         讨论完了堆栈上的内存分配之后,我们再来看下托管堆上对象的分配。我们知道在.Net中值类型是存储在堆栈上,引用类型是存储在托管堆上,上面线程堆栈的分配中,name是string类型,属于引用类型,string的分配属于比较特殊的部分,这里我推荐:

Artech的大作:字符串的驻留(String Interning)

Anytao的大作:[你必须知道的.Net]第九回:品味类型—值类型与引用类型(中)—规则无边

说明:在涛哥的这篇文章中,建议多看看精彩的评论。

         现在,假定有以下两个类定义如下:

// Employee 类定义
internal class Employee{

public int GetYearEmployed(){…}

public virtual String GetProgressReport(){…}

public static Employee Lookup(String name)(){…}

}

// Manager类定义 继承自 Employee
internal sealed class Manager:Employee{

public override String GetProgressReport(){…}

}

         现在Windows进程已经启动,CLR已经加载完成,托管对已初始化,而且已经创建好了一个线程连同他的1MB的堆栈控件。该线程已经执行了一些代码, 现在马上就要调用M3代码。图5展示了当前的状况。M3方法包含的代码演示了CLR是如何工作的。我们平时不会写这样的代码,因为它们实际上并不做任何有 用的事情。图5: 

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

现在我们来讨论下这些类型对象。在创建对象的时候,所有对象除了包含实例成员外,都会再包含两个额外的成员:类型对象指针同步块索引。 从上图中可以看出,Employee和Manager类型对象都有这两个成员。定义一个类型时,可以在类型的内部定义静态数据字段。为这些静态数据字段提 供支援的字节是在类型对象自身中分配的。在每个类型对象中,最后都包含一个方法表。在方法表中,类型中定义的每个方法都有一个对应的纪录项。

Employee 定义了三个方法,所以在它的方法表中有三个纪录项。同理,Manager只定义了一个方法,所以在Manager的方法表中只有一个纪录项。

         现在,当CLR确定方法需要的所有类型对象都已创建,而且M3的代码已经编译后,就允许线程开始执行M3的本地代码。M3的开场白代码执行时,必须从线程 的堆栈中为局部变量分配内存(引用类型存储引用,引用指向对象所在托管堆的偏移地址,此时尚未在托管堆创建对象,所以会赋值为null,在用new新增对 象后,才会指向新对象的引用地址,值类型存储变量本身,),在图中代码中,CLR会自动将所有局部变量初始化为null或0。图7: 

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

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

         注意,此时,e不再引用创建的第一个Manager对象。事实上,由于没有变量引用这个对象,所以它是将来进行垃圾收集时的主要候选对象。

         M3的下一行代码调用Employee的非虚实例方法GetYearEmployed。在调用一个非虚实例方法时,CLR会找到与发出调用的变量的的类型 对应的类型对象。在本例中,e被定义成一个Employee(假如Employee类型没有定义这个方法,则会回溯类层次结构,在基类查找)。然 后,CLR在类型对象的方法表中找到引用了被调用方法的纪录项,对方法进行JIT编译,然后调用JIT编译过的代码,就本例来说,假定该方法返回5,这个 整数保存在year中。结果如图10: 

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

         注意,如果Employee的Lookup方法发现Joe只是一个Employee,而不是一个Manager,Lookup会在内部构造一个 Employee对象,它的类型对象指针将引用Employee类型对象。这样一类,最终执行的就是Emplouee的 GetProgressReport实现,而不是Manager的GetProgressReport实现。

         至此,已经讨论了源代码、IL和JIT编译的代码之间的关系,还讨论了线程的对战、参数、局部变量、以及如何引用托管堆上的对象。我们还知道对象中包含一个指针,它指向对象的类型对象(类型对象中包含静态字段、方法表、类型对象指针和同步块索引)。还讨论了CLR如何调用静态方法、非虚方法以及虚实例方法。理解这些后,可以深刻认识到CLR的工作方式。这些知识会带来很大的帮助。

         接下来再更深层一点,看看CLR内部发生的事情。从前面几个图中,我们可以看到Employee和Manager创建类型对象时,必须初始化这些成员。那 么,具体初始化什么呢?CLR开始在一个进程中运行时,它会立即为System.Type类型创建一个特殊的类型对象。Employee和Manager 类型对象是该类型的实例。因此,他们的类型对象指针会初始化成Type类型对象的引用。如图12: 

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

结束语

         现在,我们总算理解了CLR的整个类型对象及其工作方式,System.Object的GetType方法返回的是存储在指定对象的“类型对象指针”,这样,就可以判断系统中任何对象(包括类型对象)的真实类型。

posted on 2013-11-05 17:23  zzunstu  阅读(275)  评论(0编辑  收藏  举报