原文: Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects文章讨论了: SystemDomain, SharedDomain和Default Domain 对象布局和其他的内存细节 方法表布局 方法分派文章使用的技术: .NET Framework C#因为公共语言运行时(CLR)即将成为在Windows上创建应用程序的主角级基础架构, 多掌握点关于CLR的深度认识会帮助你构建高效的, 工业级健壮的应用程序. 在这篇文章中, 我们会浏览,调查CLR的内在本质, 包括对象实例布局, 方法表的布局, 方法分派, 基于接口的分派, 和各种各样的数据结构.我们会使用由C#写成的非常简单的代码示例, 所以任何对编程语言的隐式引用都是以C#语言为目标的. 讨论的一些数据结构和算法会在Microsoft® .NET Framework 2.0中改变, 但是绝大多数的概念是不会变的. 我们会使用Visual Studio® .NET 2003 Debugger和debugger extension Son of Strike (SOS)来窥视一些数据结构. SOS能够理解CLR内部的数据结构, 能够dump出有用的信息. 通篇, 我们会讨论在Shared Source CLI(SSCLI)中拥有相关实现的类, 你可以从msdn.microsoft.com/net/sscli下载到它们. 图表1 会帮助你在搜索一些结构的时候到SSCLI中的信息.图表1 SSCLI索引
在我们开始前请注意, 这篇文章提供的信息仅适用于在x86平台架构下的.NET Framework 1.1(有可能多数信息对于Shared Source CLI 1.0中, 一些互操作情形下的多数值得注意的异常来说, 也还是正确的). 对于.NET Framework 2.0来说, 很多信息可能会改变, 所以不要创建依赖于这些内部结构不会改变的软件.CLR辅助程序创建的域=================在CLR执行第一行托管代码之前, 它先创建三个应用程序域. 其中的两个是从托管代码中产生的, 是透明的, 甚至对于CLR宿主来说都是不可见的. 这两个domain只能通过CLR bootstrap进程创建出来, 这个进程受助于两个垫板作用一样的dll文件, mscoree.dll和mscorwks.dll(当是多处理器系统的时候, 为mscorsvr.dll). 在图表2中, 你可以看到System Domain和Shared Domain, 这两个都是Singleton的(只用唯一一个实例). 第三个域是default app domain, 它是一个AppDomain类的实例, 也是唯一命名的domain. 对于简单的CLR宿主程序, 比方说控制台程序, default domain的名字是由可执行镜像的名字组成的. 其他的域可以通过在托管代码中使用AppDomain.CreateDomain方法, 或者在非托管宿主代码中通过调用ICORRuntimeHost接口, 来创建. 类似ASP.NET这样的复杂的宿主, 基于Web Site的数目来创建多个域.图表2 CLR辅助程序创建的域
系统域-System Domain===================系统域负责创建和初始化Shared Domain和default appdomain. 它加载系统库mscorlib.dll到Shared Domain中. 它还显式或隐式的保持着进程范围的字符串的字面值.保存字符串的字面值(string interning)在.NET Framework 1.1中是一项有点点笨拙的优化特性, 因为CLR并不给assemblies机会来选择是否使用它. 不论如何, 它在所有的应用程序域范围内, 提供给定字符串值的唯一实例(相同值的字符串在内存中只有一份).系统域还负责生成进程范围的接口ID, 这些接口ID被用来在每一个AppDomain中创建InterfaceVtableMaps. 系统域记录并监控着进程中的所有域, 并实现了加载和卸载AppDomain的功能.共享域-Shared Domain===================所有的域-中立的代码都被加载到shared domain中.Mscorlib, 这个系统库, 是被所有的appdomain中的用户代码使用和需要的, 它会被自动的加载到SharedDomain中. 像Object, ValueType, Array, Enum, String, 还有Delegate之类的System命名空间中的基础类型, 都会在CLR辅助程序进程(CLR bootstrapping process)中, 被预先加载到SharedDomain里.用户代码(user code)也可以被加载到该域中, 方法是通过在调用CorBindToRuntimeEx方法时, 指定LoaderOptimization属性. LoaderOptimization属性是由CLR宿主应用程序指定的.控制台程序可以给应用程序的main方法编写属性来加载代码到SharedDomain中, 这个属性是System.LoaderOptimizationAttribute.共享域还管理由基地址(the base address)索引的assembly map, assembly map的功能类似于一种查找表, 这个查找表用于明确被加载到Default Domain中的assembly和在其它应用程序域的托管代码中创建的assembly的共享依赖关系.默认域(Default Domain)是非共享的用户代码加载的地方.默认域-DefaultDomain===================默认域是一个AppDomain的实例, 典型地, 应用程序代码在这个域中执行.当一些应用程序需要在运行时创建额外的appdomain的时候(比如拥有插件式架构的应用程序, 或者是正在生成相当大量的运行时代码的应用程序), 多数的应用程序会在他们的生命期中创建一个这样的一个域: 所有执行在这个域中的代码都是在域层次上进行了上下文绑定的.如果一个应用程序有多个appdomain, 那么任何跨domain的访问都要通过.NET Remoting proxies(.net远程代理).额外的domain内的上下文边界可以通过继承自System.ContextBoundObject的类型来创建.每一个AppDomain都有自己的SecurityDescriptor, SecurityContext和DefaultContext, 同样的, 还有自己的加载者堆(高频堆, 低频堆, 和Stub堆), 句柄表(句柄表, 大对象堆句柄表), 接口虚表映射管理器(Interface Vtable Map Manager), 和Assembly Cache.加载者堆-LoaderHeaps====================加载者堆是用来加载各种各样的CLR runtime artifacts【译注:artifact这里可以理解为一种structure】和优化artifacts的, 这些artifacts在域的生命期中都存在.这些堆按照可以预见的大小的块来增长, 从而最小化内存碎片.加载者堆与GC堆(在对称的多处理器情况下, 是多重堆-multiple heap) 不同, 不同之处在于GC堆保存对象实例,而加载者堆保存的是整个类型系统.经常访问到的artifact比如MethodTables, MeghodDescs, FieldDescs和InterfaceMaps, 都在高频堆上分配, 而不那么经常访问的数据结构比如说EEClass和ClassLoader还有ClassLoader的查找表, 在低频堆(LowFrequencyHeap)上分配.StubHeap保存着很多stub, stub可以帮助代码访问security (CAS), COM wrapper calls和P/Invoke.简单在高层次上过了一遍各种域和加载者堆之后, 我们现在来看一下以一个简单应用程序为上下文背景的, 这些结构的物理细节. 见图表3. 我们将程序的执行中断在了"mc.Method1();", 并且使用SOS debugger extension的DumpDomain命令dump出了域的信息, 这里是编辑过的输出结果:---------------------------------------------------!DumpDomainSystem Domain: 793e9d58, LowFrequencyHeap: 793e9dbc, HighFrequencyHeap: 793e9e14, StubHeap: 793e9e6c,Assembly: 0015aa68 [mscorlib], ClassLoader: 0015ab40Shared Domain: 793eb278, LowFrequencyHeap: 793eb2dc,HighFrequencyHeap: 793eb334, StubHeap: 793eb38c,Assembly: 0015aa68 [mscorlib], ClassLoader: 0015ab40Domain 1: 149100, LowFrequencyHeap: 00149164,HighFrequencyHeap: 001491bc, StubHeap: 00149214,Name: Sample1.exe, Assembly: 00164938 [Sample1],ClassLoader: 00164a78 图表3 Sample1.exeusing System;public interface MyInterface1{ void Method1(); void Method2();}public interface MyInterface2{ void Method2(); void Method3();}class MyClass : MyInterface1, MyInterface2{ public static string str = "MyString"; public static uint ui = 0xAAAAAAAA; public void Method1() { Console.WriteLine("Method1"); } public void Method2() { Console.WriteLine("Method2"); } public virtual void Method3() { Console.WriteLine("Method3"); }}class Program{ static void Main() { MyClass mc = new MyClass(); MyInterface1 mi1 = mc; MyInterface2 mi2 = mc; int i = MyClass.str.Length; uint j = MyClass.ui;mc.Method1(); mi1.Method1(); mi1.Method2(); mi2.Method2(); mi2.Method3(); mc.Method3(); }}我们的控制台程序, Sample1.exe, 被加载到名为"Sample1.exe"的AppDomain中.Mscorlib.dll被加载到SharedDomain中, 但是它还是被列在SystemDomain中, 因为他是核心的系统库.每个域中都分配了自己的高频堆,低频堆,和stub堆. 系统域和共享域使用同样的ClassLoader, 而Default AppDomain使用的是它自己的ClassLoader.输出结果中并没有显示出加载者堆保存的尺寸和已经committed的尺寸. 高频堆初始保留尺寸是32KB, committed的尺寸是4KB. 低频堆和Stub堆初始保留尺寸是8KB, committed的尺寸是4KB.在SOS输出中同样没有显示出来的是InterfaceVtableMap堆. 每个域都有一个InterfaceVtableMap堆(后面再用的时候就简写为IVMap), 在域初始化阶段它,被创建在自己的加载者堆上. IVMap堆初始保留大小为4KB, 初始committed的大小是4KB. 我们将在接下来的部分中,探索类型布局的时候,讨论IVMap的重要性.图表2 展示了默认的进程堆, JIT代码堆, GC堆(针对小对象的), 和大对象堆(针对大于等于85000字节的对象的), 通过他们来说明了:这些堆和加载者堆在语义上的不同.just-in-time(JIT)编译器生成x86指令, 并且把它们存储在JIT代码堆上.GC堆和大对象堆都是垃圾收集堆, 托管对象是在这些堆上实例化出来的.类型基础- Type Fundamentals========================类型是在.NET编程中的基础单位. 在C#中, 一个类型使用关键字class, struct,和interface来声明. 多数的类型是显示的由程序员来创建的, 然而, 在特殊的互操作情形下, 或者在远程对象激活(.NET remoting)场景中, .NET CLR隐式的生成一些类型. 这些生成的类型包括COM和运行时可调用的包装器, 还有透明的代理.(COM and Runtime Callable Wrappers and Transparent Proxies).我们接下来探索一下类型基础, 从包含一个对象引用的栈开始.(典型地, 栈是一个对象实例开始他的生命期的位置.) 代码在图表4中, 其中包括一个简单的程序, 有调用静态方法的控制台入口点. Method1创建了一个类型为SmallClass的的实例, SmallClass中包括一个字节数组, 我们通过这个数组来demo在大对象堆上的对象实例的创建. 代码的实用价值不高, 但是足够为我们的讨论服务了.图表 4 大对象和小对象-Large Objects and Small Objectsusing System;class SmallClass{ private byte[] _largeObj; public SmallClass(int size) { _largeObj = new byte[size]; _largeObj[0] = 0xAA; _largeObj[1] = 0xBB; _largeObj[2] = 0xCC; } public byte[] LargeObj { get { return this._largeObj; } }}class SimpleProgram{ static void Main(string[] args) { SmallClass smallObj = SimpleProgram.Create(84930, 10, 15, 20, 25); return; } static SmallClass Create(int size1, int size2, int size3, int size4, int size5) { int objSize = size1 + size2 + size3 + size4 + size5; SmallClass smallObj = new SmallClass(objSize); return smallObj; }}图表5 显示了断点在Create方法中的"return smallObj;"语句的栈的一个快照(snapshot), 这是一个典型的fastcall的栈框架. (Fastcall是.NET的一种调用约定, 在这种调用约定下, 传递给函数的参数在可能的情况下会通过寄存器来传递, 其他的参数从右至左的压入栈中供函数调用, 函数调用结束后, 由函数自身将栈中的参数清除.)值类型变量objSize存储在栈框架之内.类似smallObj的引用类型以一个固定的大小(4字节的双字), 存储在栈中, 双字的内容是在普通GC堆上的对象实例的地址.在传统C++中, 这是一个对象指针; 在托管世界中, 这是一个对象引用. 不论如何, 它包含对象实例的地址. 我们将会对存储在对象引用的地址中的数据结构使用术语ObjectInstance.图表5 简单程序的栈框架和堆-SimpleProgram Stack Frame and Heaps
| Item | SSCLI Path |
|---|---|
| AppDomain | \sscli\clr\src\vm\appdomain.hpp |
| AppDomainStringLiteralMap | \sscli\clr\src\vm\stringliteralmap.h |
| BaseDomain | \sscli\clr\src\vm\appdomain.hpp |
| ClassLoader | \sscli\clr\src\vm\clsload.hpp |
| EEClass | \sscli\clr\src\vm\class.h |
| FieldDescs | \sscli\clr\src\vm\field.h |
| GCHeap | \sscli\clr\src\vm\gc.h |
| GlobalStringLiteralMap | \sscli\clr\src\vm\stringliteralmap.h |
| HandleTable | \sscli\clr\src\vm\handletable.h |
| InterfaceVTableMapMgr | \sscli\clr\src\vm\appdomain.hpp |
| Large Object Heap | \sscli\clr\src\vm\gc.h |
| LayoutKind | \sscli\clr\src\bcl\system\runtime\interopservices\layoutkind.cs |
| LoaderHeaps | \sscli\clr\src\inc\utilcode.h |
| MethodDescs | \sscli\clr\src\vm\method.hpp |
| MethodTables | \sscli\clr\src\vm\class.h |
| OBJECTREF | \sscli\clr\src\vm\typehandle.h |
| SecurityContext | \sscli\clr\src\vm\security.h |
| SecurityDescriptor | \sscli\clr\src\vm\security.h |
| SharedDomain | \sscli\clr\src\vm\appdomain.hpp |
| StructLayoutAttribute | \sscli\clr\src\bcl\system\runtime\interopservices\attributes.cs |
| SyncTableEntry | \sscli\clr\src\vm\syncblk.h |
| System namespace | \sscli\clr\src\bcl\system |
| SystemDomain | \sscli\clr\src\vm\appdomain.hpp |
| TypeHandle | \sscli\clr\src\vm\typehandle.h |