深入探索.NET框架内部了解CLR如何创建运行时对象

来源:互联网 发布:淘宝卖家骂人会扣分吗 编辑:程序博客网 时间:2024/06/09 14:21

随着通用语言运行时(CLR)即将成为在Windows®下开发应用程序的首选架构,对其进行深入理解会帮助你建立有效的工业强度的应用程序。在本文中,我们将探索CLR内部,包括对象实例布局,方法表布局,方法分派,基于接口的分派和不同的数据结构。

我们将使用C#编写的简单代码示例,以便任何固有的语言语法含义是C#的缺省定义。某些此处讨论的数据结构和算法可能会在Microsoft®.NET Framework 2.0中改变,但是主要概念应该保持不变。我们使用Visual Studio®.NET 2003调试器和调试器扩展Son of Strike (SOS)来查看本文讨论的数据结构。SOS理解CLR的内部数据结构并输出有用信息。请参考“Son of Strike”补充资料,了解如何将SOS.dll装入Visual Studio .NET 2003调试器的进程空间。本文中,我们将描述在共享源代码CLIShared Source CLISSCLI)中有相应实现的类,在我们开始前,请注意:本文提供的信息只对在X86平台上运行的.NET Framework 1.1有效(对于Shared Source CLI 1.0也大部分适用,只是在某些交互操作的情况下必须注意例外),对于.NET Framework 2.0会有改变,所以请不要在构建软件时依赖于这些内部结构的不变性。

CLR启动程序(Bootstrap)创建的域

CLR执行托管代码的第一行代码前,会创建三个应用程序域。其中两个对于托管代码甚至CLR宿主程序(CLR hosts)都是不可见的。它们只能由CLR启动进程创建,而提供CLR启动进程的是shim——mscoree.dllmscorwks.dll (在多处理器系统下是mscorsvr.dll)。正如2所示,这些域是系统域(System Domain)和共享域(Shared Domain),都是使用了单件(Singleton)模式。第三个域是缺省应用程序域(Default AppDomain),它是一个AppDomain的实例,也是唯一的有命名的域。对于简单的CLR宿主程序,比如控制台程序,默认的域名由可执行映象文件的名字组成。其它的域可以在托管代码中使用AppDomain.CreateDomain方法创建,或者在非托管的代码中使用ICORRuntimeHost接口创建。复杂的宿主程序,比如ASP.NET,对于特定的网站会基于应用程序的数目创建多个域。

 

2 CLR启动程序创建的域

系统域(System Domain

系统域负责创建和初始化共享域和默认应用程序域。它将系统库mscorlib.dll载入共享域,并且维护进程范围内部使用的隐含或者显式字符串符号。

字符串驻留(string interning)是.NET Framework 1.1中的一个优化特性,它的处理方法显得有些笨拙,因为CLR没有给程序集机会选择此特性。尽管如此,由于在所有的应用程序域中对一个特定的符号只保存一个对应的字符串,此特性可以节省内存空间。

系统域还负责产生进程范围的接口ID,并用来创建每个应用程序域的接口虚表映射图(InterfaceVtableMaps)的接口。系统域在进程中保持跟踪所有域,并实现加载和卸载应用程序域的功能。

共享域(Shared Domain

所有不属于任何特定域的代码被加载到系统库SharedDomain.Mscorlib,对于所有应用程序域的用户代码都是必需的。它会被自动加载到共享域中。系统命名空间的基本类型,如Object, ValueType, Array, Enum, String, and Delegate等等,在CLR启动程序过程中被预先加载到本域中。用户代码也可以被加载到这个域中,方法是在调用CorBindToRuntimeEx时使用由CLR宿主程序指定的LoaderOptimization特性。控制台程序也可以加载代码到共享域中,方法是使用System.LoaderOptimizationAttribute特性声明Main方法。共享域还管理一个使用基地址作为索引的程序集映射图,此映射图作为管理共享程序集依赖关系的查找表,这些程序集被加载到默认域(DefaultDomain)和其它在托管代码中创建的应用程序域。非共享的用户代码被加载到默认域。

默认域(Default Domain

默认域是应用程序域(AppDomain)的一个实例,一般的应用程序代码在其中运行。尽管有些应用程序需要在运行时创建额外的应用程序域(比如有些使用插件,plug-in,架构或者进行重要的运行时代码生成工作的应用程序),大部分的应用程序在运行期间只创建一个域。所有在此域运行的代码都是在域层次上有上下文限制。如果一个应用程序有多个应用程序域,任何的域间访问会通过.NET Remoting代理。额外的域内上下文限制信息可以使用System.ContextBoundObject派生的类型创建。每个应用程序域有自己的安全描述符(SecurityDescriptor),安全上下文(SecurityContext)和默认上下文(DefaultContext),还有自己的加载器堆(高频堆,低频堆和代理堆),句柄表,接口虚表管理器和程序集缓存。

加载器堆(Loader Heaps

加载器堆的作用是加载不同的运行时CLR部件和优化在域的整个生命期内存在的部件。这些堆的增长基于可预测块,这样可以使碎片最小化。加载器堆不同于垃圾回收堆(或者对称多处理器上的多个堆),垃圾回收堆保存对象实例,而加载器堆同时保存类型系统。经常访问的部件如方法表,方法描述,域描述和接口图,分配在高频堆上,而较少访问的数据结构如EEClass和类加载器及其查找表,分配在低频堆。代理堆保存用于代码访问安全性code access securityCAS)的代理部件,如COM封装调用和平台调用(P/Invoke)。

从高层次了解域后,我们准备看看它们在一个简单的应用程序的上下文中的物理细节,见3。我们在程序运行时停在mc.Method1(),然后使用SOS调试器扩展命令DumpDomain来输出域的信息。。这里是编辑后的输出:

!DumpDomain

System Domain: 793e9d58, LowFrequencyHeap: 793e9dbc,

HighFrequencyHeap: 793e9e14, StubHeap: 793e9e6c,

Assembly: 0015aa68 [mscorlib], ClassLoader: 0015ab40

Shared Domain: 793eb278, LowFrequencyHeap: 793eb2dc,

HighFrequencyHeap: 793eb334, StubHeap: 793eb38c,

Assembly: 0015aa68 [mscorlib], ClassLoader: 0015ab40

Domain 1: 149100, LowFrequencyHeap: 00149164,

HighFrequencyHeap: 001491bc, StubHeap: 00149214,

Name: Sample1.exe, Assembly: 00164938 [Sample1],

ClassLoader: 00164a78

我们的控制台程序,Sample1.exe,被加载到一个名为“Sample1.exe”的应用程序域。Mscorlib.dll被加载到共享域,不过因为它是核心系统库,所以也在系统域中列出。每个域会分配一个高频堆,低频堆和代理堆。系统域和共享域使用相同的类加载器,而默认应用程序使用自己的类加载器。

输出没有显示加载器堆的保留尺寸和已提交尺寸。高频堆的初始化大小是32KB,每次提交4KBSOS的输出也没有显示接口虚表堆(InterfaceVtableMap)。每个域有一个接口虚表堆(简称为IVMap),由自己的加载器堆在域初始化阶段创建。IVMap保留大小是4KB,开始时提交4KB。我们将会在后续部分研究类型布局时讨论IVMap的意义。

2显示默认的进程堆,JIT代码堆,GC堆(用于小对象)和大对象堆(用于大小等于或者超过85000字节的对象),它说明了这些堆和加载器堆的语义区别。即时(just-in-timeJIT)编译器产生x86指令并且保存到JIT代码堆中。GC堆和大对象堆是用于托管对象实例化的垃圾回收堆。

类型原理

类型是.NET编程中的基本单元。在C#中,类型可以使用classstructinterface关键字进行声明。大多数类型由程序员显式创建,但是,在特别的交互操作(interop)情形和远程对象调用(.NET Remoting)场合中,.NET CLR会隐式的产生类型,这些产生的类型包含COM和运行时可调用封装及传输代理(Runtime Callable Wrappers and Transparent Proxies)。

我们通过一个包含对象引用的栈开始研究.NET类型原理(典型地,栈是一个对象实例开始生命期的地方)。4中显示的代码包含一个简单的程序,它有一个控制台的入口点,调用了一个静态方法。Method1创建一个SmallClass的类型实例,该类型包含一个字节数组,用于演示如何在大对象堆创建对象。尽管这是一段无聊的代码,但是可以帮助我们进行讨论。

5显示了停止在Create方法“return smallObj;”代码行断点时的fastcall栈结构(fastcall.NET的调用规范,它说明在可能的情况下将函数参数通过寄存器传递,而其它参数按照从右到左的顺序入栈,然后由被调用函数完成出栈操作)。本地值类型变量objSize内含在栈结构中。引用类型变量如smallObj以固定大小(4字节DWORD)保存在栈中,包含了在一般GC堆中分配的对象的地址。对于传统C++,这是对象的指针;在托管世界中,它是对象的引用。不管怎样,它包含了一个对象实例的地址,我们将使用术语对象实例(ObjectInstance)描述对象引用指向地址位置的数据结构。

 

5SimpleProgram的栈结构和堆

一般GC堆上的smallObj对象实例包含一个名为_largeObj的字节数组(注意,图中显示的大小为85016字节,是实际的存贮大小)。CLR对大于或等于85000字节的对象的处理和小对象不同。大对象在大对象堆(LOH)上分配,而小对象在一般GC堆上创建,这样可以优化对象的分配和回收。LOH不会压缩,而GC堆在GC回收时进行压缩。还有,LOH只会在完全GC回收时被回收。

smallObj的对象实例包含类型句柄(TypeHandle),指向对应类型的方法表。每个声明的类型有一个方法表,而同一类型的所有对象实例都指向同一个方法表。它包含了类型的特性信息(接口,抽象类,具体类,COM封装和代理),实现的接口数目,用于接口分派的接口图,方法表的槽(slot)数目,指向相应实现的槽表。

方法表指向一个名为EEClass的重要数据结构。在方法表创建前,CLR类加载器从元数据中创建EEClass4中,SmallClass的方法表指向它的EEClass。这些结构指向它们的模块和程序集。方法表和EEClass一般分配在共享域的加载器堆。加载器堆和应用程序域关联,这里提到的数据结构一旦被加载到其中,就直到应用程序域卸载时才会消失。而且,默认的应用程序域不会被卸载,所以这些代码的生存期是直到CLR关闭为止。

对象实例

正如我们说过的,所有值类型的实例或者包含在线程栈上,或者包含在GC堆上。所有的引用类型在GC堆或者LOH上创建。6显示了一个典型的对象布局。一个对象可以通过以下途径被引用:基于栈的局部变量,在交互操作或者平台调用情况下的句柄表,寄存器(执行方法时的this指针和方法参数),拥有终结器(finalizer)方法的对象的终结器队列。OBJECTREF不是指向对象实例的开始位置,而是有一个DWORD的偏移量(4字节)。此DWORD称为对象头,保存一个指向SyncTableEntry表的索引(从1开始计数的syncblk编号。因为通过索引进行连接,所以在需要增加表的大小时,CLR可以在内存中移动这个表。SyncTableEntry维护一个反向的弱引用,以便CLR可以跟踪SyncBlock的所有权。弱引用让GC可以在没有其它强引用存在时回收对象。SyncTableEntry还保存了一个指向SyncBlock的指针,包含了很少需要被一个对象的所有实例使用的有用的信息。这些信息包括对象锁,哈希编码,任何转换层(thunking)数据和应用程序域的索引。对于大多数的对象实例,不会为实际的SyncBlock分配内存,而且syncblk编号为0。这一点在执行线程遇到如lock(obj)或者obj.GetHashCode的语句时会发生变化,如下所示:

SmallClass bj = new SmallClass()

// Do some work here

lock(obj) { /* Do some synchronized work here */ }

obj.GetHashCode();

在以上代码中,smallObj会使用0作为它的起始的syncblk编号。lock语句使得CLR创建一个syncblk入口并使用相应的数值更新对象头。因为C#lock关键字会扩展为try-finally语句并使用Monitor类,一个用作同步的Monitor对象在syncblk上创建。堆GetHashCode的调用会使用对象的哈希编码增加syncblk

SyncBlock中有其它的域,它们在COM交互操作和封送委托(marshaling delegates)到非托管代码时使用,不过这和典型的对象用处无关。

类型句柄紧跟在对象实例中的syncblk编号后。为了保持连续性,我会在说明实例变量后讨论类型句柄。实例域(Instance field)的变量列表紧跟在类型句柄后。默认情况下,实例域会以内存最有效使用的方式排列,这样只需要最少的用作对齐的填充字节。7的代码显示了SimpleClass包含有一些不同大小的实例变量。

8显示了在Visual Studio调试器的内存窗口中的一个SimpleClass对象实例。我们在7return语句处设置了断点,然后使用ECX寄存器保存的simpleObj地址在内存窗口显示对象实例。前4个字节是syncblk编号。因为我们没有用任何同步代码使用此实例(也没有访问它的哈希编码),syncblk编号为0。保存在栈变量的对象实例,指向起始位置的4个字节的偏移处。字节变量b1,b2,b3b4被一个接一个的排列在一起。两个short类型变量s1s2也被排列在一起。字符串变量str是一个4字节的OBJECTREF,指向GC堆中分配的实际的字符串实例。字符串是一个特别的类型,因为所有包含同样文字符号的字符串,会在程序集加载到进程时指向一个全局字符串表的同一实例。这个过程称为字符串驻留(string interning),设计目的是优化内存的使用。我们之前已经提过,在NET Framework 1.1中,程序集不能选择是否使用这个过程,尽管未来版本的CLR可能会提供这样的能力。

所以默认情况下,成员变量在源代码中的词典顺序没有在内存中保持。在交互操作的情况下,词典顺序必须被保存到内存中,这时可以使用StructLayoutAttribute特性,它有一个LayoutKind的枚举类型作为参数。LayoutKind.Sequential可以为被封送(marshaled)数据保持词典顺序,尽管在.NET Framework 1.1中,它没有影响托管的布局(但是.NET Framework 2.0可能会这么做)。在交互操作的情况下,如果你确实需要额外的填充字节和显示的控制域的顺序,LayoutKind.Explicit可以和域层次的FieldOffset特性一起使用。

看完底层的内存内容后,我们使用SOS看看对象实例。一个有用的命令是DumpHeap,它可以列出所有的堆内容和一个特别类型的所有实例。无需依赖寄存器,DumpHeap可以显示我们创建的唯一一个实例的地址。

!DumpHeap -type SimpleClass

Loaded Son of Strike data table version 5 from

"C:"WINDOWS"Microsoft.NET"Framework"v1.1.4322"mscorwks.dll"

 Address       MT     Size

00a8197c 00955124       36

Last good object: 00a819a0

total 1 objects

Statistics:

      MT    Count TotalSize Class Name

 955124        1        36 SimpleClass

对象的总大小是36字节,不管字符串多大,SimpleClass的实例只包含一个DWORD的对象引用。SimpleClass的实例变量只占用28字节,其它8个字节包括类型句柄(4字节)和syncblk编号(4字节)。找到simpleObj实例的地址后,我们可以使用DumpObj命令输出它的内容,如下所示:

!DumpObj 0x00a8197c

Name: SimpleClass

MethodTable 0x00955124

EEClass 0x02ca33b0

Size 36(0x24) bytes

FieldDesc*: 00955064

      MT    Field   Offset                 Type       Attr    Value Name

00955124 400000a        4         System.Int64   instance      31 l1

00955124 400000b        c                CLASS   instance 00a819a0 str

    << some fields omitted from the display for brevity >>

00955124 4000003       1e          System.Byte   instance        3 b3

00955124 4000004       1f          System.Byte   instance        4 b4

正如之前说过,C#编译器对于类的默认布局使用LayoutType.Auto(对于结构使用LayoutType.Sequential);因此类加载器重新排列实例域以最小化填充字节。我们可以使用ObjSize来输出包含被str实例占用的空间,如下所示:

!ObjSize 0x00a8197c

sizeof(00a8197c) =       72 (    0x48) bytes (SimpleClass)

如果你从对象图的全局大小(72字节)减去SimpleClass的大小(36字节),就可以得到str的大小,即36字节。让我们输出str实例来验证这个结果:

!DumpObj 0x00a819a0

Name: System.String

MethodTable 0x009742d8

EEClass

原创粉丝点击