代码改变世界

.Net运行时的相互关系

2012-02-16 09:42  秋梧  阅读(3154)  评论(18编辑  收藏  举报
      阅读目录
  • 前言
  • 线程堆栈的分配
  • 托管堆上对象的分配
  • 结束语

 

前言

  .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方法返回的是存储在指定对象的“类型对象指针”,这样,就可以判断系统中任何对象(包括类型对象)的真实类型。

 

 如果觉得不错的话,请点击下推荐,(*^__^*) !!

转载请注明出处:http://www.cnblogs.com/enshjiang/archive/2012/02/16/2353599.html