1. 值类型实例的创建位置: 对于值类型的实例,CLR在运行时有两种分配方式:(1) 如果该值类型的实例作为类型中的方法(Method)中的局部变量,则该实例被创建在线程栈上;(2) 如果该值类型的实例作为类型的成员,则该实例作为引用类型(引用类型在GC堆或者LOH上创建)的实例的一部分,被创建在GC堆上。下面这段代码演示了这两种情况:
2. 引用类型实例的创建位置: 对于引用类型的实例,CLR在运行时也有两种分配方式:(1) 如果该引用类型的实例的Size<85000Byte,则该实例被创建在GC(Garbage Collection)堆上(当CLR在分配和回收对象时,GC可能会对GC堆进行压缩);(2) 如果该引用类型的实例的Size>=85000byte,则该实例被创建在LOH(Large Object Heap)上(LOH不会被压缩)。面这段代码演示了这两种情况:
3. 托管对象被引用的七种途径: 上面的代码片段Test2中,也演示了引用托管对象的两种途径:(1) intArr随Test2实例创建的同时,被创建在GC堆上,由GC堆上的类型(Test2)实例持有托管对象(int数组的实例)的引用;(2) 引用类型的变量o存在线程栈上,由线程栈上的局部变量(o)持有托管对象(Object实例)的引用。其实还有以下五种途径可以持有托管对象的引用:(3) LOH堆上的实例(原理同1);(4) 在与非托管语句交互操作或者P/Invoke情况下的句柄表(Handle Table);(5) 寄存器,例如执行实例方法时的this指针和方法参数(IL中的call、callvirl指令是将函数参数进行自右往左的压栈处理通过栈实现传递参数;而fastcall指令则可以最多将两个参数分别保存到ECX和EDX两个寄存器中,通过寄存器来实现参数传递,以提高程序的性能;这里的方法参数是指后一种fastcall的情况);(6) 拥有终结器(finalizer)方法的对象的终结器队列;(7) 所属类型的HandleTable(对象创建时,该HandleTable将持有一个弱引用Weak Reference)。下图演示了这几种情况:
4. 托管对象的结构: 从上图中,我们可以看到,托管对象的引用并不是指向对象的起始位置,而是相对起始位置有+4Byte(DWord)的偏移量,这4个Byte称为对象头。下面转载一段对该对象头的介绍:对象头保存一个间接指向SyncTableEntry表的索引(从1开始计数的syncblk编号)。SyncTableEntry维护一个反向的弱引用,以便CLR可以跟踪SyncBlock的所有权。弱引用让GC可以在没有其它强引用存在时回收对象。SyncTableEntry还保存了一个指向SyncBlock的指针,包含了很少需要被一个对象的所有实例使用的有用的信息。这些信息包括对象锁,哈希编码,任何转换层(thunking)数据和应用程序域的索引。对于大多数的对象实例,不会为实际的SyncBlock分配内存,而且syncblk编号为0。这一点在执行线程遇到如lock(obj)或者obj.GetHashCode的语句时会发生变化,如下所示:
在以上代码中,smallObj会使用0作为它的起始的syncblk编号。lock语句使得CLR创建一个syncblk入口并使用相应的数值更新对象头。因为C#的lock关键字会扩展为try-finally语句并使用Monitor类,一个用作同步的Monitor对象在syncblk上创建。堆GetHashCode的调用会使用对象的哈希编码增加syncblk。在SyncBlock中有其它的域,它们在COM交互操作和封送委托(marshaling delegates)到非托管代码时使用,不过这和典型的对象用处无关。
紧跟在syncblk编号后的是一个TypeHandle句柄(占4个byte),有点像C++中的虚方法表指针,但实际上这个TypeHandle指向的MethodTable比C++中的虚方法表复杂得多(这里先不介绍,以后有机会再介绍)。
在TypeHandle之后才开始存放实例字段的实例。默认情况下(即相当于在类上运用特性StructLayoutAttribute(LayoutKind.Auto)),实例字段会以内存最有效使用的方式排列,以便更有效地使用内存,排序之后字段在内存中的布局顺序跟字段在类中声明的顺序不一定相同,这样也给我们计算托管对象的Size带来了不便....
<<未完待续>>下一篇将继续讨论如何获得托管对象的Size。