原文: Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objectshtml
文章討論了:程序員
SystemDomain, SharedDomain和Default Domain算法
對象佈局和其餘的內存細節chrome
方法表佈局編程
方法分派bootstrap
文章使用的技術:數組
.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索引
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 |
在咱們開始前請注意, 這篇文章提供的信息僅適用於在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出了域的信息, 這裏是編輯過的輸出結果:
---------------------------------------------------
!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
圖表3 Sample1.exe
using 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 Objects
using 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
smallObj對象的實例(object instance), 存儲在普通GC堆上, 其中包含一個字節數組, 叫作_largeObj, 這個字節數組的大小是85000字節(注意, 圖中顯示的是85016字節, 這是真實存儲的空間大小.)
CLR對待大小>=85000字節的對象, 跟對待比這小的對象的方式不一樣. 大對象分配在Large Object Heap(LOH)中, 而小對象是建立在普通GC堆上的. 由於普通GC堆對於對象的分配和垃圾收集是有優化的(因此適合存儲小對象的效率高). 大對象堆是沒有壓縮的(夯實的), 而GC堆在GC垃圾收集發生的時候是壓縮的. 更重要的是, 大對象堆(LOH)僅在徹底垃圾回收的時候才被釋放(LOH is only collected on full GC collections).
smallObj的ObjectInstance包含TypeHandle(類型句柄), TypeHandle指向相關連的類型的MethodTable.
任何一個聲明瞭的類型都僅有一個MethodTable, 而且全部一樣類型的對象的實例都指向同一份MethodTable.
MethodTable包含
關於這種類型的信息(屬於哪個? interface, abstract class, concrete class, COM Wrapper仍是Proxy).
實現了的接口數量
爲了方法分配而設立的接口映射表(interface map for the method dispatch)
方法表中的槽的數量(方法表中方法的數量)(number of slots in the method table)
一張盡是指向方法的實現的槽的表格
一個由MethodTable指向的重要的數據結構, 是EEClass. 在MethodTable展開以前, CLR類加載器(class loader)從元數據(Metadata)中建立出EEClass. 在圖表4中, SmallClass的MethodTable指向它的EEClass. 這些結構指向他們的模塊和assembly.
MethodTable和EEClass典型地分配在具體域的加載者堆上. 字節數組(Byte[])是一個特例. 方法表MethodTable和EEClass分配在共享域中的加載者堆上.
加載者堆是appdomain-specific的, 任何這裏提到的數據結構(MethodTable和EEClass)一旦加載起來就不會被移除, 除非它的AppDomain被卸載掉.
一樣, 默認的appdomain也不能被卸載掉, 所以代碼直到CLR關閉都還存在着.
對象實例-ObjectInstance
====================
正如咱們提到的, 全部的值類型要麼以inline(內聯)地存儲在線程棧中, 要麼內聯地存儲在GC堆當中. 全部的引用類型都是在GC堆上或者大對象堆建立的. 圖表6 顯示了一個典型的對象實例的佈局.
一個對象能夠被如下的結構引用:
1. 基於棧的局部變量;
2. interop或者P/Invoke情形下的句柄表;
3. 寄存器(寄存器中的內容是:執行方法時的this指針或方法參數)
4. 服務於擁有finalizer方法的對象的finalizer queue.
OBJECTREF並不指向Object Instance的首地址, 而是指向一個以DWORD(4個字節)爲單位的一個偏移量.
這個DWORD的偏移量叫作Object Header, 而且擁有一個指向SyncTableEntry表的索引值(a 1-based syncblk number). 經過索引的鏈鎖效應, CLR在須要增加內存尺寸的狀況下, 能夠在內存中自由的移動SyncTableEntry表.
SyncTableEntry中保存着一個指回對象的weak reference, 這樣CLR就能夠追蹤到SyncBlock的全部權(屬於哪一個對象). Weak Reference可讓GC在沒有其餘強引用的狀況下, 收集到這個對象.
SyncTableEntry中還存着一個指向SyncBlock的指針, SyncBlock中保存着有用的信息, 可是這些信息不多被全部的對象實例使用到. 這些信息包括對象鎖(object's lock), 它的Hash Code, 一些轉換數據(thunking data), 和它的AppDomain index.
對多數的對象實例來講, 他們當中沒有爲SyncBlock分配的存儲空間, syncblk number是0. 然而,當線程執行遇到例如lock(obj), 或者obj.GetHashCode的時候, 就不一樣了. 就像下面的代碼同樣:
SmallClass obj = new SmallClass() // Do some work here lock(obj) { /* Do some synchronized work here */ } obj.GetHashCode();
圖表6 對象實例佈局-Object Instance Layout
在這段代碼中, smallObj會使用0(沒有syncblk)作爲它起始時的syncblk number. 那句lock語句引起了CLR建立一個syncblk entry的動做, 並用相應的數值來更新對象的object header. 因爲C#的lock關鍵字能夠展開爲一個try-finally塊, 用來使用Monitor類, 因此Monitor對象在爲同步化(synchronization)而準備的syncblk中建立出來. 對GetHashCode方法的調用把對象的hash code填入到syncblk中.
SyncBlock中還有些其餘的數據域, 它們有的用在COM的interop上, 有的用在針對非託管代碼的marshaling delegate上. 可是這些數據域跟典型的對象使用無關.
TypeHandle的位置是緊跟着ObjectInstance中的syncblk number的. 爲了保持連續性, 我會在詳細闡述變量實例以後, 討論TypeHandle.
在TypeHandle以後緊跟着一個實例的變量列表域. 默認狀況下, 這個實例域會按照能讓內存高效使用的方式來壓縮, 或者按照能讓內存讀取高效的對齊來作最小程度的填充. 圖表7中的代碼顯示了一個SimpleClass, 該class擁有不少包含不一樣大小的變量的實例.
圖表7 擁有實例變量的SimpleClass- SimpleClass with Instance Variables
class SimpleClass { private byte b1 = 1; // 1 byte private byte b2 = 2; // 1 byte private byte b3 = 3; // 1 byte private byte b4 = 4; // 1 byte private char c1 = 'A'; // 2 bytes private char c2 = 'B'; // 2 bytes private short s1 = 11; // 2 bytes private short s2 = 12; // 2 bytes private int i1 = 21; // 4 bytes private long l1 = 31; // 8 bytes private string str = "MyString"; // 4 bytes (only OBJECTREF) //Total instance variable size = 28 bytes static void Main() { SimpleClass simpleObj = new SimpleClass(); return; } }
圖表8 顯示出了SimpleClas對象實例在Visual Studio Debugger內存窗口中的一個例子. 咱們在圖表7的return語句上下斷點, 而後用在寄存器ECX中存儲的simpleObj的地址來在內存窗口中顯示對象的實例. 頭4個字節的塊就是syncblk number. 由於咱們以前沒有在任何synchronizing的代碼中使用這個實例, 它被設置爲0. 以變量形式存在棧中的的對象引用, 指向偏移量爲4的四個字節. 字節變量b1, b2, b3和b4都被一個挨着一個的排放着. 兩個short型的變量被放在一塊兒. 字符串型的變量str是一個4字節的OBJECTREF, 指向GC堆中字符串實際存在的地址. 字符串是一種特殊的類型, 在assembly加載的進程中, 它們的全部包含着相同內容的實例, 都會被指向相同的在全局字符串表中的那一份惟一實例. 這個進程叫作string interning, 是用來優化內存的使用的.
如同咱們以前提到的, 在.NET Framework1.1中, 一個assbmbly不可能從這個interning process中退出(opt out of), 儘管將來的CLR版本可能會修改這種能力.
圖表8 調試器內存窗口中的object instance- Debugger Memory Window for Object Instance
因此, 默認狀況下, 在源代碼中聲明的成員變量的字面順序, 在內存中並不會被保留下來. 在Interop的場景下, 變量的字面順序必須被正向的依次放到內存中, StructLayoutAttribute屬性能夠用來完成這個設定, 該屬性接受LayoutKind枚舉類型的變量做爲參數. LayoutKind.Sequential會爲marshaled的數據設定字面的順序, 儘管在.NET Framework 1.1中,這個設定還不會對託管佈局生效.(.NET Framework 2.0就會了). 在interop場景下, 你實在須要額外的填充(padding)和顯式的對於數據域順序的控制時, LayoutKind.Explicit能夠和FieldOffset這個修飾符結合起來在field level幫助您達到目的.
看過了原始內存的內容, 讓咱們用SOS來看一下對象實例吧. 一個有用的命令是DumpHeap, 它能夠列出針對某一類型的全部堆中的內容, 還有這一類型的全部實例. 不依賴寄存器, DumpHeap命令能夠show出咱們建立的惟一實例的地址.
---------------------------------------------------!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的instance中只包含一個DWORD OBJECTREF. SimpleClass的實例變量只佔28個字節. 剩下的八個字節是由TypeHandle(4字節), 和syncblk number(4字節)組成的.
找到了simpleObj實例的地址後, 讓咱們用DumpObj命令來dump出這個實例吧, 以下:
---------------------------------------------------!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. 因爲class loader從新安排了實例的數據域, 因此填充(padding)的部分達到了最小化. 咱們能夠用ObjSize命令來dump出實例佔用空間的圖示. 這裏是輸出結果:
---------------------------------------------------!ObjSize 0x00a8197c sizeof(00a8197c) = 72 ( 0x48) bytes (SimpleClass)
******************************************************************************************************
Son of Strike
在本文中, SOS調試器擴展時用來展示CLR數據結構內容的.
它是.NET Framework安裝程序的一部分, 位置在%windir%\Microsoft.NET\Framework\v1.1.4322.
在你加載SOS到你的進程以前, 在Visual Studio .NET的工程屬性裏選擇容許託管代碼調試.(enable managed debugging)
添加SOS.dll所在的文件夾到環境變量中. 要在斷點時, 加載SOS.dll, 打開Debug | Windows | Immediate. 在Immediate窗口中,
執行.load sos.dll命令. 用!help命令來獲得關於debugger 命令的幫助. 更多關於SOS的信息, 參見the June 2004Bugslayer column
******************************************************************************************************
若是你從object graph的大小(72字節)減去SimpleClass實例的大小(36字節), 你會獲得變量str的長度(36字節). 讓咱們經過dump出這個字符串實例來確認一下吧. 輸出結果以下:
---------------------------------------------------!DumpObj 0x00a819a0 Name: System.String MethodTable 0x009742d8 EEClass 0x02c4c6c4 Size 36(0x24) bytes
若是你把字符串的長度(36字節)加上SimpleClass實例的大小(36字節), 你就獲得了對象的整個大小(72字節), 正與前面ObjSize命令的結果相同.
注意, ObjSize方法並不包括由syncblk架構佔用的內存. 在.NET Framework 1.1中, CLR並不瞭解被非託管資源佔據的內存, 好比說GDI對象, COM對象, 文件句柄等等. 所以, 他們的大小是不會被這個命令的結果報告中反映出來的.
TypeHandle, 是一個指向MethodTable的指針, 它的位置緊跟在syncblk number以後. 在一個對象實例建立以前, CLR會查詢加載了的類型,
若是這個類型沒有找到就加載它, 得到類型的MethodTable的地址, 建立對象實例, 而後填充對象的TypeHandle值. JIT編譯器產生的代碼使用TypeHandle來尋找MethodTable, 用於實現method dispatching. CLR能夠在任什麼時候候經過TypeHandle指向的MethodTable來反向追溯已經加載了的類型.
方法表- MethodTable
=================
任何一個類或者接口, 當他們加載到AppDomain當中的時候, 都會由一個叫作MethodTable數據結構來表明. 在對象的第一個實例都還沒被加載的狀況下, 建立出一個MethodTable是類的加載動做的執行成果。
ObjectInstance表明的是對象的狀態, MethodTable表明的是對象的行爲.
MethodTable把object instance與language compiler-generated memory-mapped metadata structures, 經過EEClass聯繫起來. 在MethodTable中的信息和metadata structure能夠在託管代碼中經過Systen.Type來訪問到.
在託管代碼中, 一個指向MethodTable的指針能夠經過Type.RuntimeTypeHandle屬性來得到. TypeHandle, 存在於ObjectInstance中, 它指向一個偏移量, 這個偏移量是從MethodTable的首地址算起的. 這裏的偏移量默認是12字節。這開頭的12個字節包含GC的一些信息, 咱們並不打算在這裏討論這些信息.
圖表9展示了一個典型的MethodTable的佈局. 咱們會show一些重要的TypeHandle的數據域, 可是爲了一個更完整的列表, 仍是看圖表吧. 讓咱們從Base Instance Size開始吧, 由於它與運行時的內存輪廓有直接關係.
圖表9 方法表佈局- MethodTable Layout
基本實例尺寸-Base Instance Size
===========================
基本實例尺寸是由class loader計算出來的對象的大小, 是基於代碼中的數據域聲明來計算的. 如同前面討論的, 當前GC的實現須要一個對象的大小至少是12個字節. 若是一個類沒有任何的實例數據域被定義, 它會白白的用前4個字節做爲佔位字節. 剩下的8字節會被Object Header(可能包括一個syncblk number), 和TypeHandle佔據. 再次強調, 對象的大小是能夠被StructLayoutAttribute屬性影響的.
看看圖表3(MyClass和兩個接口)中MyClass的MethodTable的內存快照吧(Visual Studio .NET 2003 memory window). 請拿它和SOS生成的輸出結果進行比較. 在圖表9中, 對象大小是在4字節的偏移量的地方的, 其值爲12 (0x0000000C)字節. 下面是 SOS中命令DumpHeap的輸出結果
--------------------------------------!DumpHeap -type MyClass Address MT Size 00a819ac 009552a0 12 total 1 objects Statistics: MT Count TotalSize Class Name 9552a0 1 12 MyClass
方法槽表-Method Slot Table
======================
在MethodTable中內嵌的是一張指向各自方法的方法描述器(MethodDesc)的指針組成的表格. 他們的存在容許了這個類型擁有一些行爲. Method Slot表是根據按以下順序實現了的方法的線性表來建立的: 繼承的虛函數, 新虛函數, 實例方法, 和靜態方法.(Inherited virtuals, Introduced virtuals, Instance Methods, and Static Methods).
ClassLoader遍歷當前類的, 基類的, 和接口的metadata, 而後建立出method table. 在layout process, 任何重載了的虛函數都會被取代, 取代並隱藏父類的方法, 建立新的slot, 必要的狀況下複製slot. 對slot的複製對於建立一個illusion是必不可少的, 所謂illusion是指每一個接口都有他本身的小虛函數表. 然而, 複製的slot指向相同的物理實現. MyClass有三個實例方法, 一個類構造函數(.cctor), 和一個對象構造函數(.ctor). 對象構造函數是由C#編譯器爲全部沒有顯式定義構造函數的對象,自動生成的. 類構造函數是由編譯器生成的, 由於咱們定義並初始化了一個靜態變量. 圖表10 顯示出了MyClass
的方法表的佈局. 佈局顯示出了10個方法, 爲了IVMap,Method2有重複的slot,這個重複將會在後面介紹. 圖表11顯示了MyClass的方法表在編輯事後的SOS dump.
圖表10 MyClass的方法表佈局
圖表11 SOS Dump of MyClass Method Table
-----------------------------------------------!DumpMT -MD 0x9552a0 Entry MethodDesc Return Type Name 0097203b 00972040 String System.Object.ToString() 009720fb 00972100 Boolean System.Object.Equals(Object) 00972113 00972118 I4 System.Object.GetHashCode() 0097207b 00972080 Void System.Object.Finalize() 00955253 00955258 Void MyClass.Method1() 00955263 00955268 Void MyClass.Method2() 00955263 00955268 Void MyClass.Method2() 00955273 00955278 Void MyClass.Method3() 00955283 00955288 Void MyClass..cctor() 00955293 00955298 Void MyClass..ctor()
任何類型的頭四個方法永遠都會是ToString, Equals, GetHashCode, 和Finalize.
他們是從System.Object繼承來的虛方法. Method2的slot是duplicated的, 可是兩者都指向相同的方法描述器(method descriptor). 顯式編碼的.cctor會和靜態方法分爲一組, .ctor會和實例方法分爲一組. (The explicitly coded .cctor and .ctor will be grouped with static methods and instance methods, respectively.)
方法描述- MethodDesc
======================
方法描述(Method Descriptor)(MethodDesc)是方法實現的的一種封裝, CLR是知道,瞭解這種封裝的. 有好幾種Method Descriptor, 他們的存在不只使得調用託管代碼的實現更容易, 並且使得對interop的實現的調用也變得容易了一些. 在這篇文章中, 咱們只研究以圖表3的代碼爲上下文的託管MethodDesc.
一個MethodDesc是做爲類加載過程的一部分(class loading process)而產生出來的, MethodDesc初始狀況下指向中間語言Intermediate Language(IL).
每個MethodDesc都被一個叫作PreJitStub的填充, PreJitStub負責觸發JIT的編譯過程.
圖表12展現了一個典型的佈局. 方法表的slot entry實際指向PreJitStub, 而不是指向實際的MethodDesc. 這是一個從MethodDesc算起, 負5個字節的偏移量, 而且是每一個方法繼承的8個字節的填充的一部分.
那五個字節包含調用PreJitStub函數的指令. 這5字節的偏移量能夠從SOS的DumpMT命令的結果輸出中看到(圖表11的MyClass), 由於MethodDesc老是在MethodSlot Table入口指向的位置日後數5個字節的位置上. 緊接着第一個調用, 一個對於JIT編譯函數的調用被觸發. 在編譯結束以後, 這五個字節所包含的調用指令會被覆蓋爲一條無條件轉移到JIT編譯的x86的代碼的jmp指令.
圖表12 Method Descriptor
在圖表12中的Method Table Slot入口指向的代碼的反彙編結果中, 顯示出了對於PreJitStub的調用. 這是一個刪節了的Method2的在JIT以前的反彙編代碼:
-------------------------------!u 0x00955263 Unmanaged code 00955263 call 003C3538 ;call to the jitted Method2() 00955268 add eax,68040000h ;ignore this and the rest ;as !u thinks it as code
如今讓咱們執行這個方法而且反彙編一樣的地址:
--------------------------------!u 0x00955263 Unmanaged code 00955263 jmp 02C633E8 ;call to the jitted Method2() 00955268 add eax,0E8040000h ;ignore this and the rest ;as !u thinks it as code
只有從給定地址開始的五個字節內容是代碼, 後面包含的是Method2的MethodDesc的數據. 這裏的"!u"命令對這一點是不知情的, 因此生成了一些胡言亂語, 故爾你能夠忽略那五個字節以後的任何東西(它們不是指令).
CodeOrIL在JIT編譯以前, 包含方法實現的IL碼的Relative Virtual Address(RVA). 這個數據域被一個標誌位標識: 其中存儲的是IL. 一經要求, CLR完成了編譯以後, CLR會使用JIT處理過的代碼的地址來更新這個數據域.讓咱們從列表中選擇一個方法, 而後使用DumpMT命令dump出來JIT編譯以前和以後的MethodDesc吧:
---------------------------------!DumpMD 0x00955268 Method Name : [DEFAULT] [hasThis] Void MyClass.Method2() MethodTable 9552a0 Module: 164008 mdToken: 06000006 Flags : 400 IL RVA : 00002068
編譯以後, MethodDesc看起來像這樣:
---------------------------------!DumpMD 0x00955268 Method Name : [DEFAULT] [hasThis] Void MyClass.Method2() MethodTable 9552a0 Module: 164008 mdToken: 06000006 Flags : 400 Method VA : 02c633e8
在方法描述器(method descriptor)中的Flags數據域會根據方法的類型來編碼, 所謂方法類型是指: 靜態方法, 實例方法, 接口方法, 或者是COM實現方法.
讓咱們看看MethodTable的複雜的另外一面吧: Interface implementation.
託管環境下, Interface implementation被實現的看起來簡單一些, 達到這個效果的方式是把全部的複雜度都吸取到佈局過程當中. 下一步, 咱們將要show給你接口是如何佈局的, 還有基於接口的方法分派(method dispatching)是如何工做的.
接口虛表映射和接口映射- Interface Vtable Map and Interface Map
=====================================================
在MethodTable中, 偏移量爲12的位置, 存儲着一個重要的指針, IVMap. 如圖表9, IVMap指向一個AppDomain等級的映射表, 該映射表以一個進程等級的接口ID爲索引. 這個接口ID是在接口類型第一次加載的時候生成的. 任何一個接口的實現都會有一個IVMap的入口. 若是MyInterface1被兩個類實現了, 那麼在接口的IVMap中就會有兩個入口. 入口會指回到MyClass的方法表內嵌的sub-table的開頭, 見圖表9. 這個是method dispatching發生時, 基於接口的索引. IVMap是根據內嵌在方法表中的Interface Map的信息而建立出來的. Interface Map是在佈局方法表的過程當中, 根據類的metadata建立出來的. 一旦類型加載結束, 只有IVMap會在method dispatching中使用到.
Interface Map在偏移量28的位置, 它會指向內嵌在MethodTable中的InterfaceInfo的入口. 這樣, 對於兩個被MyClass實現了的接口中的任何一個接口, 都會有兩個入口了.
第一個InterfaceInfo入口的頭四個字節指向MyInterface1的TypeHandle(參考圖表9和圖表10).
接下來的WORD(兩字節)被Flags佔據(其中0是指繼承自父類, 1指的是被當前類實現).
從Flags再接下來的WORD是Start Slot, 經過它, class loader得以編排接口實現的sub-table. 對於MyInterface1, 這個值是4, 意味slot 5 和slot 6指向implementation. 對於MyInterface2, 這個值是6, 因此, slot 7 和slot 8 指向implementation. 若是必要的話, ClassLoader會複製這些slot來建立illusion. 這裏的illusion指在物理映射到相同的method descriptor的同時, 任何一個接口都獲得了本身的實現. 在MyClass中, MyInterface1.Method2 和MyInterface2.Method2會指向相同的實現.
基於接口的方法分派(method dispatching)是經過IVMap發生的, 而直接的方法分派是經過各自槽的MethodDesc的地址發生的. 如前所述, .NET Framework使用fastcall這種調用約定. 若是可能的話, 頭兩個參數典型地被傳入ECX和EDX寄存器(譯註:參見文章彙編語言基礎之六- 調用棧和各類調用約定的總結對比)(最左邊參數, 經過ECX傳遞, 第二個, 經過EDX傳遞).
對象實例的方法中, 第一個參數永遠是this指針, 經過ECX來傳遞. 下面的語句中"mov ecx, esi"展現了這一點.
-------------------------------------mi1.Method1(); mov ecx,edi ;move "this" pointer into ecx mov eax,dword ptr [ecx] ;move "TypeHandle" into eax mov eax,dword ptr [eax+0Ch] ;move IVMap address into eax at offset 12 mov eax,dword ptr [eax+30h] ;move the ifc impl start slot into eax call dword ptr [eax] ;call Method1 mc.Method1(); mov ecx,esi ;move "this" pointer into ecx cmp dword ptr [ecx],ecx ;compare and set flags call dword ptr ds:[009552D8h];directly call Method1
這裏的反彙編代碼代表了在對MyClass的實例方法的直接調用中, 並無使用偏移量. JIT編譯器將MethodDesc的地址直接的用在了代碼中. 基於接口的方法分派是經過IVMap來發生的, 而且比直接分派須要多一些指令. 一條用來拿到IVMap的地址, 另外一條拿到MethodTable中接口實現的start slot. 而且, 將一個對象實例轉換爲一個接口也僅僅是拷貝一下這個指針到目標變量當中就能夠了. 在圖表3 【譯註:原文中這個地方時圖表2,顯然有錯誤,應該是圖表3】中, 語句 "mi1 = mc;" 只用了一條指令就把mc中的OBJECTREF拷貝給了mi1.
虛擬分派- Virtual Dispatch
========================
讓咱們來看一下虛擬分派吧, 恩, 再比較一下跟基於接口的直接分派有什麼不一樣. 下面是圖表3中對於虛方法MyClass.Method3調用的反彙編代碼:
-----------------------------mc.Method3(); Mov ecx,esi ;move "this" pointer into ecx Mov eax,dword ptr [ecx] ;acquire the MethodTable address Call dword ptr [eax+44h] ;dispatch to the method at offset 0x44
虛擬分派老是經過一個定死了的slot number來發生, 與給定的類的實現層次的MethodTable指針無關. 在MethodTable佈局的時候, ClassLoader替換掉父類的實現, 而使用子類的實現. 結果, 對於父類對象的方法調用被分派到子類對象的實現上. 反彙編代碼展示了分派經過slot number 8來發生的, debugger memory window中的DumpMT命令的輸出結果也是這樣(見圖表10).
靜態變量- Static Variables
========================
靜態變量是MethodTable數據結構的重要組成部分. 他們在method table slot數組以後被分配在MethodTable上. 全部原始的靜態類型都是內聯的, 而靜態值類型(結構體), 引用類型是經過AppDomain的handle table(句柄表)上的OBJECTREF來引用的. MethodTable中的OBJECTREF指向AppDomain的句柄表中的OBJECTREF, 這個OBJECTREF會使得堆上建立出來的對象實例一直存活下去, 直到AppDomain被卸載掉. 在圖表9中, 一個靜態的字符串變量, str, 指向句柄表中的OBJECTREF, 而這個OBJECTREF指向GC堆上的MyString.
EEClass
========================
EEClass在MethodTable被建立以前就存在了, 在與MethodTable結合的時候, 是一個類型聲明的CLR版. 實際上, EEClass和MethodTable在邏輯上是一個數據結構(他們共同表明一個單個類型), 他們中的內容是基於使用頻率不一樣而分開的. 很是常用的數據域存在MethodTable中, 而不太常用的數據域存在EEClass中. 因此, JIT編譯函數須要的信息(好比說names, fields, 和offsets)就存在EEClass中, 然而, 運行時須要的信息(好比虛表slot和GC信息)就存在MethodTable中.
加載到AppDomain中的任何一個類型都有一個EEClass. 這裏所說的類型包括: 接口, 類, 抽象類, 數組, 和結構體. 任何一個EEClass 都是被執行引擎跟蹤的樹的節點. 爲了諸如:加載類, 佈局MethodTable, 辨別類型, 類型轉換,這樣的目的, CLR使用這個網絡來導航到須要的EEClass結構. EEClass之間的孩子到父親的關係是基於繼承關係來建立的, 然而, 從父親到孩子的關係是創建在繼承關係和類的加載順序的聯合的基礎上的. 隨着在託管代碼的執行, 新的EEClass節點被一個個的添加, 舊的節點之間的關係被不斷修補, 新的節點關係也被創建起來.
網絡中EEClass的兄弟之間還有水平的關係呢. EEClass有三個數據域被用來創建加載起來的類型之間的關係: ParentClass, SiblingChain, 和ChildrenChain. 參見圖表13, 來看看以圖表4爲上下文的MyClass的EEClass的扼要圖解.
圖表13展示了一些與咱們的討論相關聯的數據域. 由於咱們忽略了佈局中的一些數據域, 咱們並無在這張圖表中展示出真是的偏移量. EEClass有一個針對MethodTable的環形引用. EEClass也指向在默認AppDomain的高頻堆中分配的MethodDesc塊. 進程堆上有一個對FieldDesc對象列表的引用, 它提供了在MethodTable建立時的field佈局信息. EEClass在AppDomain的低頻堆上, 這樣,操做系統能夠更好的進行內存的頁面管理, 所以減少了working set.
圖表13 EEClass佈局- EEClass Layout
================================
圖表13中的其餘數據域光看名字就能理解他們在MyClass(圖表3)上下文中的做用了. 讓咱們看一下使用SOS工具dump出的EEClass的真實物理內存吧. 在mc.Method1這一行設定斷點後, 運行圖表3中的代碼.首先, 經過運行命令!Name2EE得到MyClass的EEClass的地址:
------------------------------------
!Name2EE C:\Working\test\ClrInternals\Sample1.exe MyClass MethodTable: 009552a0 EEClass: 02ca3508 Name: MyClass
Name2EE命令的第一個參數是模塊名稱, 這個模塊名稱能夠經過DumpDomain命令來得到. 如今咱們有了EEClass的地址了, 咱們來dump出EEClass的內容:
-----------------------------------------
!DumpClass 02ca3508 Class Name : MyClass, mdToken : 02000004, Parent Class : 02c4c3e4 ClassLoader : 00163ad8, Method Table : 009552a0, Vtable Slots : 8 Total Method Slots : a, NumInstanceFields: 0, NumStaticFields: 2,FieldDesc*: 00955224 MT Field Offset Type Attr Value Name 009552a0 4000001 2c CLASS static 00a8198c str 009552a0 4000002 30 System.UInt32 static aaaaaaaa ui
圖表13和DumpClass的輸出結果看起來本質上是相同的. Metadata token(mdToken)表明着模塊PE文件映射在內存中的metadata表的index.
指向System.Object. Sibling 鏈(圖表13)的Parent類, 說明了他的加載是加載Program類的結果形成的.
MyClass有8個vtable slot(能夠被虛擬分派的方法). 儘管Method1和Method2並非虛擬方法, 它們在經過接口來分派的時候,仍是被認爲是虛函數並添加到列表中. 算上.cctor和.ctor到列表中, 這樣你就一共有10個方法了. 這個類有兩個靜態域, 都被列在後面了. MyClass沒有實例域. 其他的域都挺自我說明問題的.
結論
=========
咱們一塊兒遊歷了一下CLR中最重要的一些內部信息. 很顯然, 還有不少方面須要被覆蓋到, 而且還要更加深刻, 可是咱們但願這篇文章能夠給你一個CLR怎麼工做的大體印象. 這裏展示的不少的信息在將來版本的CLR和.NET Framework中可能會改變, 可是儘管這篇文章覆蓋到的數據結構更改了, 概念是不會變的.
-----------------------------------------------------------
做者介紹
Hanu Kommalapati is an Architect at Microsoft Gulf Coast District (Houston). In his current role at Microsoft, he helps enterprise customers in building scalable component frameworks based on the .NET Framework. He can be reached at hanuk@microsoft.com.
Tom Christian is an Escalation Engineer with Developer Support at Microsoft, working with ASP.NET and the .NET debugger extension for WinDBG (sos/psscor). He is based in Charlotte, NC and can be contacted attomchris@microsoft.com.
後記: 不少學習.NET的資料都推薦這篇文章, <Windows用戶態程序高效排錯>中稱這篇文章是字字珠璣, 因而學友捨得花兩天的時間翻譯,校對這篇文章. 主要目的是讓本身能更深入的理解文章所述及的技術細節. 但願你能和我同樣, 看了這篇文章後能有所收穫.
其實文章中涉及到的技術不只僅是標題部分列出來的.NET Framework和C#。 若是讀者懂一些彙編的基礎知識,將會有更好的理解。個人博文中有個彙編基礎的系列, 我以爲做爲這篇文章的一點知識準備挺適合的.
看了這篇文章以後, 相信你也以爲.net不像從前那麼神祕了,對麼? :)
好多長句, 理清楚從句之間的從屬和修飾關係很累人. 若是忠於原文, 光看懂長句就要花上一兩分鐘, 因此仍是把長句子拆成了許多短句. 方便你們快速獲取一些印象. 有的時候概念仍是英文的好懂些, 就沒翻譯的那麼完全. 有些中英文概念都在緊隨其後的括號中有另一個的註解. 第一次出現的名詞, 通常用英文直接寫出, 後面概念重複了的時候才適當翻譯成中文. 總之, 以易讀爲目標組織語言.
不知道你們的習慣如何, 我的感受在銀屏上讀文章就但願一目瞭然, 因此爲了概念和結構的清晰, 原文的某些段落被用換行拆開了.
翻譯水平有限, 技術水平也有限, 讓讀者見笑了. 原文的連接在頁面的頂部. 若是有困惑能夠對照着看看.
歡迎批評指正!
posted @ 2009-11-11 23:13 中道學友 閱讀(1144) 評論(0) 編輯