欢迎阅读本系列其他文章:
【读书笔记】.NET本质论第一章 The CLR as a Better COM
【读书笔记】.NET本质论第二章-Components(Part One)
【读书笔记】.NET本质论第二章-Components(Part Two,public key)
【读书笔记】.NET本质论第二章-Components(Part Three,CLR Loader)
【读书笔记】.NET本质论第二章-Components(Part Four,Assembly Resolver)
【读书笔记】.NET本质论第三章-Type Basics(Part 1)
【读书笔记】.NET本质论第三章-Type Basics(Part 2)
【读书笔记】.NET本质论第三章-Type Basics(Part 3)
【读书笔记】.NET本质论第四章-Programming with Type(Part One)
本章的第一篇简单的说了一下值类型和引用类型,以及编写代码时对对象日后在内存中布局的影响。本篇会重点关注对象加载到内存后是个什么样子,是个什么样的结构,而这些结构在运行时又起到什么作用。
下图是一个对象在内存中的结构:
所有引用类型,分配在托管堆上时,都会分配额外的两个字段,同步块索引与方法表指针,在32位中,这两个字段各占4个字节。紧跟在方法表后面的就是该对象的实例字段。
关于同步块索引这个字段,我已经用了三篇文章来介绍了,在这里就不再重复了:
那本篇文章就主要讨论这个方法表指针以及相关的东西。
读过《.NET本质论》的读者也许会发现,原书说的在MethodTable Pointer的地方是htype,也就是所谓的“类型句柄”,那类型句柄和方法表指针又是什么关系呢?这个在文章稍后再做探讨。我们在这里就先只关注方法表指针和方法表。
其实方法表(MethodTable)这个名字并不能表达出它本身的含义,可以说还让人产生误解。实际上方法表根本就不是一个“表”,从主体的结构上也没有看到任何表的意思(实际上所谓的方法的列表时附加在MethodTable主体结构之后的,要访问方法表,只需要用this+offset,这样做纯粹是为了性能的考虑,因为一个类型的方法个数不定,常规做法肯定是在另外一个地方动态的开辟一块内存保存这个方法的列表,然后在MethodTable内包含一个指向这个列表的指针,因为这个方法列表在方法调用的时候需要频繁使用,性能非常重要,如果使用指针的方法,就多了一层,是间接的访问,而现在这种设计只需要加一个偏移就够了),关于方法表结构的更详细描述在后面的文章中,下图是一个简图:
方法表实际上包含的是对一个类型的详细描述,比如该类型的父类,接口,当然还包括该类型的方法。实际上和方法表共存的还有一个东西:EEClass。EEClass和方法表所要表示的东西一样,都是根据类型的元数据构建而来。那为什么要分两个东西呢?对于一个类型的元数据来说,有一部分是经常用到的,比如Virtual Disaptch,这些数据称之为“Hot Data”,还有一部分却不经常使用,这部分就放在EEClass中,而方法表中有指向EEClass的指针。但是方法表和EEClass并不是一一对应的关系,比如一个泛型类型,如果泛型的参数是引用类型的话(比如MyClass<string>和MyClass<Object>),它们的EEClass是共享的,而方法表却不同。
方法表在多态、类型检查、JIT中都起着非常重要的作用,是.NET的核心之一。当第一次执行某方法时,CLR会检查该方法内要使用的所有类型,如果这些类型没有加载则会加载这些类型,加载类型的时候还要看看该类型的程序集是否加载了,如果没加载则首先加载程序集,然后读取程序集中的元数据,先构建EEClass,然后构建方法表。在AppDomain中这样的俩个堆:High-Frequency-Heap和Low-Frequency-Heap,方法表就会加载到High-Frequency-Heap,EEClass加载到Low-Frequency-Heap。注意,这两个Heap都不属GC的管辖,所以一个类型加载后,不会因为不再使用而被清理,只会等到AppDomain卸载后才会清理。对于普通的WinForm程序,默认情况下只有等到程序关闭的时候AppDomain才会Unload,对于ASP.NET或寄宿在其他Host中的程序(比如Sql Server 2005开始提供对CLR的支持),这个就要看Host到底是如何调度的。
上面光用文字说了这么多,下面就用Visual Studio+SOS建立一些感性的认识,为后面根据Rotor的代码分析方法表建立基础。
Demo1:
1: namespace Yuyijq.Study.MethodTable
2: {
3: interface IFoo
4: {
5: void Run();
6: }
7: class Foo : IFoo
8: {
9: private int _instanceField = 5;
10: private static int _staticField = 6;
11:
12: #region IFoo Members
13:
14: public void Run()
15: {
16:
17: }
18:
19: #endregion
20: }
21: class Program
22: {
23: static void Main(string[] args)
24: {
25: IFoo f = new Foo();
26: f.Run();
27:
28: Console.ReadLine();
29: }
30: }
31: }
看看这里的Foo类,实现了IFoo接口,有一个实例字段,一个静态字段,实现了IFoo的Run方法。我们在Console.ReadLine()处设置断点,通过下面这两个命令我们找到了Foo类型的方法表地址(00463120):
1: !dso
2: PDB symbol for mscorwks.dll not loaded
3: OS Thread Id: 0x1514 (5396)
4: ESP/REG Object Name
5: 002eefa8 01a92de4 System.Object[] (System.String[])
6: 002ef32c 01a92df4 Yuyijq.Study.MethodTable.Foo
7: ....
8:
9: !do 01a92df4
10: Name: Yuyijq.Study.MethodTable.Foo
11: MethodTable: 00463120
12: .....
使用dumpmt –md 00463120,我们看看MethodTable可以给我们哪些信息:
1: !dumpmt -md 00463120
2: EEClass: 00461360
3: Module: 00462c5c
4: Name: Yuyijq.Study.MethodTable.Foo
5: mdToken: 02000003 (E:\Study\ConsoleApplication10\ConsoleApplication10\bin\Debug\Yuyijq.Study.MethodTable.exe)
6: BaseSize: 0xc
7: ComponentSize: 0x0
8: Number of IFaces in IFaceMap: 1
9: Slots in VTable: 7
10: --------------------------------------
11: MethodDesc Table
12: Entry MethodDesc JIT Name
13: 66046a90 65ec1248 PreJIT System.Object.ToString()
14: 66046ab0 65ec1250 PreJIT System.Object.Equals(System.Object)
15: 66046b20 65ec1280 PreJIT System.Object.GetHashCode()
16: 660b74c0 65ec12a4 PreJIT System.Object.Finalize()
17: 0046c038 004630fc JIT Yuyijq.Study.MethodTable.Foo.Run()
18: 0046c040 00463108 JIT Yuyijq.Study.MethodTable.Foo..ctor()
19: 0046c048 00463114 JIT Yuyijq.Study.MethodTable.Foo..cctor()
在这里可以知道EEClass的地址是00461360,该类型实现了几个接口:Number of IFaces in IFaceMap:1。方法表的方法数目Slots in VTable: 7,以及下面跟着的方法表。还可以知道该类型的实例将占用多少字节的内存 BaseSize: 0xC(12个字节,一个int类型的实例字段和额外开辟的8个字节)。而我们非常关心的就是下面的MethodDesc Table(这个在后面的文章再深入讨论)。
除此之外,我们还可以使用dumpclass 00461360命令查看EEClass的细节:
1: !dumpclass 00461360
2: Class Name: Yuyijq.Study.MethodTable.Foo
3: mdToken: 02000003 (E:\Study\ConsoleApplication10\ConsoleApplication10\bin\Debug\Yuyijq.Study.MethodTable.exe)
4: Parent Class: 65e83a88
5: Module: 00462c5c
6: Method Table: 00463120
7: Vtable Slots: 5
8: Total Method Slots: 7
9: Class Attributes: 100000
10: NumInstanceFields: 1
11: NumStaticFields: 1
12: MT Field Offset Type VT Attr Value Name
13: 660eab0c 4000001 4 System.Int32 1 instance _instanceField
14: 660eab0c 4000002 1c System.Int32 1 static 6 _staticField
1: !dumpclass 65e83a88
2: Class Name: System.Object
3: mdToken: 02000002 (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
4: Parent Class: 00000000
5: Module: 65e81000
6: Method Table: 660e84dc
7: Vtable Slots: 4
8: Total Method Slots: a
9: Class Attributes: 102001
10: NumInstanceFields: 0
11: NumStaticFields: 0
原来是System.Object,而System.Object的Parent Class是00000000。
后记
通过前面的文字描述,和后面的调试,我想你应该对MethodTable和EEClass有一点点的了解了,而在下一篇文章中,我们会一边欣赏Rotor的源代码,一遍来讨论方法表里的各个字段有什么作用。
突然一看,文章标题虽然是【.NET本质论读书笔记】,但却已经偏离了书本的内容,请大家不要见怪,我只想以.NET本质论的结构和描述顺序作为主线来梳理CLR的一些东西。文章内容只会跟着书的主线,但不拘泥书中讨论的内容。
祝编程愉快