[原创]探索CLR原理系列(2):字段在内存中的布局 (适合老鸟,新人勿沉迷其中)
上一篇文章我们探索了类型,每一个类型在元数据的Typedef表中,会分配一个MdToken(类型标记),当你写的方法需要访问这个类型时,也是使用MdToken到相关Dll的元数据表去加载它到Load Heap,LoadHeap是用来存放类型的空间,它并不保存类型的实例.我们可以为类型定义一系列成员,包括:字段,属性,方法,事件和嵌套类,但我们跟踪类型的EEclass,发现类型中只有两类成员,字段(事件就是一个委托,而委托只是一个类型,所以事件就是一个字段而已,但表现有些特殊后续介绍)和方法(属性实际就是方法).
这篇文章就让我们一起来探索类型中很重要的一部分:字段.首先来说明一下为什么类型需要字段?
在面向对象的语言中,每一个类型或者实例化的对象都代表一个可以解决某种特定问题的实体.那么这些实体就会有描述自身当前状态的需要,如何满足这种需要呢?
类型的状态我们使用静态字段来描述,实例的状态我们使用实例字段来表述,还有一部分状态我们需要它是不可变的(常量字段).那么这三类字段在IL中如何表现,CLR有如何加载分配内存呢,下面我们一起来扒一扒字段的隐私.
我们来定义一个类型,这个类型中包括静态字段,实例字段和常量字段.这里我们只讨论字段,所以不考虑封装性.
我们来看看在CLR中一个类型的静态字段和实例字段在内存中是怎样的。CLR如何访问一个字段?先来编写一段C#代码:
那么偏移地址是怎样生成的呢?答案是IL编译时,在DLL的元数据表中还有一个表叫做ClassLayout,表中定义了相应字段的偏移地址,以上面提到的Field元数据表中,字段的标识做索引。
那么CLR如何访问字段,使通过MdToken吗?我们来看看方法执行时的汇编,就一目了然了。
TestObjectType1 t = new TestObjectType1();原来在方法执行时,使用的都是偏移地址(除反射动态查找字段以外).
字段在继承时会怎样 ?
有很多人并不是很清楚继承时字段是怎样的,比如静态字段能不能继承?下面我们专门来讨论一下这个问题。有类如下:
public class TestObjectType1我们来看看IL是怎样的。TestObjectType1在这里笔者就不列出来了,主要看它的派生类。
TypeDef #3 (02000004)我们发现在TestObjectType2中没有任何字段定义,这是为什么?为了搞明白原因,我们再次使用SOS来察看。!dumpobj 0x0260b928
这里为了证明子类和父类不是使用同一个字段,我们做一下测试:
TestObjectType1 t1 = new TestObjectType1();TestObjectType2 t2 = new TestObjectType2();
Test(ref TestObjectType1.SX);
Test(ref TestObjectType2.SX);
再次使用SOS,看看两个静态变量是否指向相同地址。
002eeda8 00520224 TestDemo1.Type.TestMain.Test(Int32 ByRef)x (0x002eeda8) = 0x003033b8
结果是什么呢?原来静态变量的继承时逻辑上的,也就是说物理上来讲父类和子类,共享共有的静态字段。
那么实例变量呢? 肯定是不共享的,大家可以自己做测试。
总结:
本篇我们探索了类型的第一种成员:字段。字段在IL编译时,会生成MdToken和偏移量,因为对于类型来说,一个类型在编译时就已经确定了字段的个数,所以偏移量对于编译器来说是已知的,字段和偏移量分别由元数据表(Field和ClassLayout)来记录。
在类型的CLR内存布局中,有一个FieldDesList,它指向类型的字段描述(主要是字段签名和偏移量).当CLR首次加载类型时,会根据元数据表来生成 FieldDesList,包括实例和静态字段,其中静态字段的偏移量是相对于方法表中最后一项方法地址而言,所以静态字段在类型方法列表的后面进行布局,也就是说你的静态字段就存储在这个空间,它是属于类型的,也就是说所有这个类型的实例共享这个静态字段。而实例字段则是和实例在一起,属于每一个实例,它的偏移量是以实例的类型指针为基址而计算的。
子类从父类派生时,所有字段全部被派生,与可访问性无关,但对于静态字段来说,子类和父类都指向相同地址,即逻辑继承,物理上则共享.
下一篇将于大家一起探讨关于类型的另一个成员:方法,内容将会非常多,也是CLR中比较复杂的设计之一.关于实例的部分,笔者将单独分出一篇来和大家一起讨论,届时我们将分析实例的生成(GC堆和栈),以及实例的销毁(垃圾回收和栈资源的释放).