日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) >

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

發(fā)布時(shí)間:2024/4/15 45 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深入探索.NET框架内部了解CLR如何创建运行时对象 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

為什么80%的碼農(nóng)都做不了架構(gòu)師?>>> ??

本文討論:

?

SystemDomain, SharedDomain, and DefaultDomain

?

對(duì)象布局和內(nèi)存細(xì)節(jié)。

?

方法表布局。

?

方法分派(Method dispatching)。

本文使用下列技術(shù):
.NET Framework, C#

本頁(yè)內(nèi)容
CLR啟動(dòng)程序(Bootstrap)創(chuàng)建的域
系統(tǒng)域(System Domain)
共享域(Shared Domain)
默認(rèn)域(Default Domain)
加載器堆(Loader Heaps)
類型原理
對(duì)象實(shí)例
方法表
基實(shí)例大小
方法槽表(Method Slot Table)
方法描述(MethodDesc)
接口虛表圖和接口圖
虛分派(Virtual Dispatch)
靜態(tài)變量
EEClass
Conclusion結(jié)論

隨著通用語(yǔ)言運(yùn)行時(shí)(CLR)即將成為在Windows?下開發(fā)應(yīng)用程序的首選架構(gòu),對(duì)其進(jìn)行深入理解會(huì)幫助你建立有效的工業(yè)強(qiáng)度的應(yīng)用程序。在本文中,我們將探索CLR內(nèi)部,包括對(duì)象實(shí)例布局,方法表布局,方法分派,基于接口的分派和不同的數(shù)據(jù)結(jié)構(gòu)。

我們將使用C#編寫的簡(jiǎn)單代碼示例,以便任何固有的語(yǔ)言語(yǔ)法含義是C#的缺省定義。某些此處討論的數(shù)據(jù)結(jié)構(gòu)和算法可能會(huì)在Microsoft? .NET Framework 2.0中改變,但是主要概念應(yīng)該保持不變。我們使用Visual Studio? .NET 2003調(diào)試器和調(diào)試器擴(kuò)展Son of Strike (SOS)來(lái)查看本文討論的數(shù)據(jù)結(jié)構(gòu)。SOS理解CLR的內(nèi)部數(shù)據(jù)結(jié)構(gòu)并輸出有用信息。請(qǐng)參考“Son of Strike”補(bǔ)充資料,了解如何將SOS.dll裝入Visual Studio .NET 2003調(diào)試器的進(jìn)程空間。本文中,我們將描述在共享源代碼CLI(Shared Source CLI,SSCLI)中有相應(yīng)實(shí)現(xiàn)的類,你可以從msdn.microsoft.com/net/sscli下載。圖1將幫助你在SSCLI的數(shù)以兆計(jì)的代碼中找到所參考的結(jié)構(gòu)。

在我們開始前,請(qǐng)注意:本文提供的信息只對(duì)在X86平臺(tái)上運(yùn)行的.NET Framework 1.1有效(對(duì)于Shared Source CLI 1.0也大部分適用,只是在某些交互操作的情況下必須注意例外),對(duì)于.NET Framework 2.0會(huì)有改變,所以請(qǐng)不要在構(gòu)建軟件時(shí)依賴于這些內(nèi)部結(jié)構(gòu)的不變性。

CLR啟動(dòng)程序(Bootstrap)創(chuàng)建的域

在CLR執(zhí)行托管代碼的第一行代碼前,會(huì)創(chuàng)建三個(gè)應(yīng)用程序域。其中兩個(gè)對(duì)于托管代碼甚至CLR宿主程序(CLR hosts)都是不可見的。它們只能由CLR啟動(dòng)進(jìn)程創(chuàng)建,而提供CLR啟動(dòng)進(jìn)程的是shim——mscoree.dll和mscorwks.dll (在多處理器系統(tǒng)下是mscorsvr.dll)。正如圖2所示,這些域是系統(tǒng)域(System Domain)和共享域(Shared Domain),都是使用了單件(Singleton)模式。第三個(gè)域是缺省應(yīng)用程序域(Default AppDomain),它是一個(gè)AppDomain的實(shí)例,也是唯一的有命名的域。對(duì)于簡(jiǎn)單的CLR宿主程序,比如控制臺(tái)程序,默認(rèn)的域名由可執(zhí)行映象文件的名字組成。其它的域可以在托管代碼中使用AppDomain.CreateDomain方法創(chuàng)建,或者在非托管的代碼中使用ICORRuntimeHost接口創(chuàng)建。復(fù)雜的宿主程序,比如ASP.NET,對(duì)于特定的網(wǎng)站會(huì)基于應(yīng)用程序的數(shù)目創(chuàng)建多個(gè)域。


圖 2?由CLR啟動(dòng)程序創(chuàng)建的域

返回頁(yè)首

系統(tǒng)域(System Domain)

系統(tǒng)域負(fù)責(zé)創(chuàng)建和初始化共享域和默認(rèn)應(yīng)用程序域。它將系統(tǒng)庫(kù)mscorlib.dll載入共享域,并且維護(hù)進(jìn)程范圍內(nèi)部使用的隱含或者顯式字符串符號(hào)。

字符串駐留(string interning)是.NET Framework 1.1中的一個(gè)優(yōu)化特性,它的處理方法顯得有些笨拙,因?yàn)镃LR沒有給程序集機(jī)會(huì)選擇此特性。盡管如此,由于在所有的應(yīng)用程序域中對(duì)一個(gè)特定的符號(hào)只保存一個(gè)對(duì)應(yīng)的字符串,此特性可以節(jié)省內(nèi)存空間。

系統(tǒng)域還負(fù)責(zé)產(chǎn)生進(jìn)程范圍的接口ID,并用來(lái)創(chuàng)建每個(gè)應(yīng)用程序域的接口虛表映射圖(InterfaceVtableMaps)的接口。系統(tǒng)域在進(jìn)程中保持跟蹤所有域,并實(shí)現(xiàn)加載和卸載應(yīng)用程序域的功能。

返回頁(yè)首

共享域(Shared Domain)

所有不屬于任何特定域的代碼被加載到系統(tǒng)庫(kù)SharedDomain.Mscorlib,對(duì)于所有應(yīng)用程序域的用戶代碼都是必需的。它會(huì)被自動(dòng)加載到共享域中。系統(tǒng)命名空間的基本類型,如Object, ValueType, Array, Enum, String, and Delegate等等,在CLR啟動(dòng)程序過程中被預(yù)先加載到本域中。用戶代碼也可以被加載到這個(gè)域中,方法是在調(diào)用CorBindToRuntimeEx時(shí)使用由CLR宿主程序指定的LoaderOptimization特性。控制臺(tái)程序也可以加載代碼到共享域中,方法是使用System.LoaderOptimizationAttribute特性聲明Main方法。共享域還管理一個(gè)使用基地址作為索引的程序集映射圖,此映射圖作為管理共享程序集依賴關(guān)系的查找表,這些程序集被加載到默認(rèn)域(DefaultDomain)和其它在托管代碼中創(chuàng)建的應(yīng)用程序域。非共享的用戶代碼被加載到默認(rèn)域。

返回頁(yè)首

默認(rèn)域(Default Domain)

默認(rèn)域是應(yīng)用程序域(AppDomain)的一個(gè)實(shí)例,一般的應(yīng)用程序代碼在其中運(yùn)行。盡管有些應(yīng)用程序需要在運(yùn)行時(shí)創(chuàng)建額外的應(yīng)用程序域(比如有些使用插件,plug-in,架構(gòu)或者進(jìn)行重要的運(yùn)行時(shí)代碼生成工作的應(yīng)用程序),大部分的應(yīng)用程序在運(yùn)行期間只創(chuàng)建一個(gè)域。所有在此域運(yùn)行的代碼都是在域?qū)哟紊嫌猩舷挛南拗啤H绻粋€(gè)應(yīng)用程序有多個(gè)應(yīng)用程序域,任何的域間訪問會(huì)通過.NET Remoting代理。額外的域內(nèi)上下文限制信息可以使用System.ContextBoundObject派生的類型創(chuàng)建。每個(gè)應(yīng)用程序域有自己的安全描述符(SecurityDescriptor),安全上下文(SecurityContext)和默認(rèn)上下文(DefaultContext),還有自己的加載器堆(高頻堆,低頻堆和代理堆),句柄表,接口虛表管理器和程序集緩存。

返回頁(yè)首

加載器堆(Loader Heaps)

加載器堆的作用是加載不同的運(yùn)行時(shí)CLR部件和優(yōu)化在域的整個(gè)生命期內(nèi)存在的部件。這些堆的增長(zhǎng)基于可預(yù)測(cè)塊,這樣可以使碎片最小化。加載器堆不同于垃圾回收堆(或者對(duì)稱多處理器上的多個(gè)堆),垃圾回收堆保存對(duì)象實(shí)例,而加載器堆同時(shí)保存類型系統(tǒng)。經(jīng)常訪問的部件如方法表,方法描述,域描述和接口圖,分配在高頻堆上,而較少訪問的數(shù)據(jù)結(jié)構(gòu)如EEClass和類加載器及其查找表,分配在低頻堆。代理堆保存用于代碼訪問安全性(code access security, CAS)的代理部件,如COM封裝調(diào)用和平臺(tái)調(diào)用(P/Invoke)。

從高層次了解域后,我們準(zhǔn)備看看它們?cè)谝粋€(gè)簡(jiǎn)單的應(yīng)用程序的上下文中的物理細(xì)節(jié),見圖3。我們?cè)诔绦蜻\(yùn)行時(shí)停在mc.Method1(),然后使用SOS調(diào)試器擴(kuò)展命令DumpDomain來(lái)輸出域的信息。(請(qǐng)查看Son of Strike了解SOS的加載信息)。這里是編輯后的輸出:

!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

我們的控制臺(tái)程序,Sample1.exe,被加載到一個(gè)名為“Sample1.exe”的應(yīng)用程序域。Mscorlib.dll被加載到共享域,不過因?yàn)樗呛诵南到y(tǒng)庫(kù),所以也在系統(tǒng)域中列出。每個(gè)域會(huì)分配一個(gè)高頻堆,低頻堆和代理堆。系統(tǒng)域和共享域使用相同的類加載器,而默認(rèn)應(yīng)用程序使用自己的類加載器。

輸出沒有顯示加載器堆的保留尺寸和已提交尺寸。高頻堆的初始化大小是32KB,每次提交4KB。SOS的輸出也沒有顯示接口虛表堆(InterfaceVtableMap)。每個(gè)域有一個(gè)接口虛表堆(簡(jiǎn)稱為IVMap),由自己的加載器堆在域初始化階段創(chuàng)建。IVMap保留大小是4KB,開始時(shí)提交4KB。我們將會(huì)在后續(xù)部分研究類型布局時(shí)討論IVMap的意義。

圖2顯示默認(rèn)的進(jìn)程堆,JIT代碼堆,GC堆(用于小對(duì)象)和大對(duì)象堆(用于大小等于或者超過85000字節(jié)的對(duì)象),它說(shuō)明了這些堆和加載器堆的語(yǔ)義區(qū)別。即時(shí)(just-in-time, JIT)編譯器產(chǎn)生x86指令并且保存到JIT代碼堆中。GC堆和大對(duì)象堆是用于托管對(duì)象實(shí)例化的垃圾回收堆。

返回頁(yè)首

類型原理

類型是.NET編程中的基本單元。在C#中,類型可以使用class,struct和interface關(guān)鍵字進(jìn)行聲明。大多數(shù)類型由程序員顯式創(chuàng)建,但是,在特別的交互操作(interop)情形和遠(yuǎn)程對(duì)象調(diào)用(.NET Remoting)場(chǎng)合中,.NET CLR會(huì)隱式的產(chǎn)生類型,這些產(chǎn)生的類型包含COM和運(yùn)行時(shí)可調(diào)用封裝及傳輸代理(Runtime Callable Wrappers and Transparent Proxies)。

我們通過一個(gè)包含對(duì)象引用的棧開始研究.NET類型原理(典型地,棧是一個(gè)對(duì)象實(shí)例開始生命期的地方)。圖4中顯示的代碼包含一個(gè)簡(jiǎn)單的程序,它有一個(gè)控制臺(tái)的入口點(diǎn),調(diào)用了一個(gè)靜態(tài)方法。Method1創(chuàng)建一個(gè)SmallClass的類型實(shí)例,該類型包含一個(gè)字節(jié)數(shù)組,用于演示如何在大對(duì)象堆創(chuàng)建對(duì)象。盡管這是一段無(wú)聊的代碼,但是可以幫助我們進(jìn)行討論。

圖5顯示了停止在Create方法“return smallObj;”代碼行斷點(diǎn)時(shí)的fastcall棧結(jié)構(gòu)(fastcall時(shí).NET的調(diào)用規(guī)范,它說(shuō)明在可能的情況下將函數(shù)參數(shù)通過寄存器傳遞,而其它參數(shù)按照從右到左的順序入棧,然后由被調(diào)用函數(shù)完成出棧操作)。本地值類型變量objSize內(nèi)含在棧結(jié)構(gòu)中。引用類型變量如smallObj以固定大小(4字節(jié)DWORD)保存在棧中,包含了在一般GC堆中分配的對(duì)象的地址。對(duì)于傳統(tǒng)C++,這是對(duì)象的指針;在托管世界中,它是對(duì)象的引用。不管怎樣,它包含了一個(gè)對(duì)象實(shí)例的地址,我們將使用術(shù)語(yǔ)對(duì)象實(shí)例(ObjectInstance)描述對(duì)象引用指向地址位置的數(shù)據(jù)結(jié)構(gòu)。


圖5 SimpleProgram的棧結(jié)構(gòu)和堆

一般GC堆上的smallObj對(duì)象實(shí)例包含一個(gè)名為_largeObj的字節(jié)數(shù)組(注意,圖中顯示的大小為85016字節(jié),是實(shí)際的存貯大小)。CLR對(duì)大于或等于85000字節(jié)的對(duì)象的處理和小對(duì)象不同。大對(duì)象在大對(duì)象堆(LOH)上分配,而小對(duì)象在一般GC堆上創(chuàng)建,這樣可以優(yōu)化對(duì)象的分配和回收。LOH不會(huì)壓縮,而GC堆在GC回收時(shí)進(jìn)行壓縮。還有,LOH只會(huì)在完全GC回收時(shí)被回收。

smallObj的對(duì)象實(shí)例包含類型句柄(TypeHandle),指向?qū)?yīng)類型的方法表。每個(gè)聲明的類型有一個(gè)方法表,而同一類型的所有對(duì)象實(shí)例都指向同一個(gè)方法表。它包含了類型的特性信息(接口,抽象類,具體類,COM封裝和代理),實(shí)現(xiàn)的接口數(shù)目,用于接口分派的接口圖,方法表的槽(slot)數(shù)目,指向相應(yīng)實(shí)現(xiàn)的槽表。

方法表指向一個(gè)名為EEClass的重要數(shù)據(jù)結(jié)構(gòu)。在方法表創(chuàng)建前,CLR類加載器從元數(shù)據(jù)中創(chuàng)建EEClass。圖4中,SmallClass的方法表指向它的EEClass。這些結(jié)構(gòu)指向它們的模塊和程序集。方法表和EEClass一般分配在共享域的加載器堆。加載器堆和應(yīng)用程序域關(guān)聯(lián),這里提到的數(shù)據(jù)結(jié)構(gòu)一旦被加載到其中,就直到應(yīng)用程序域卸載時(shí)才會(huì)消失。而且,默認(rèn)的應(yīng)用程序域不會(huì)被卸載,所以這些代碼的生存期是直到CLR關(guān)閉為止。

返回頁(yè)首

對(duì)象實(shí)例

正如我們說(shuō)過的,所有值類型的實(shí)例或者包含在線程棧上,或者包含在GC堆上。所有的引用類型在GC堆或者LOH上創(chuàng)建。圖6顯示了一個(gè)典型的對(duì)象布局。一個(gè)對(duì)象可以通過以下途徑被引用:基于棧的局部變量,在交互操作或者平臺(tái)調(diào)用情況下的句柄表,寄存器(執(zhí)行方法時(shí)的this指針和方法參數(shù)),擁有終結(jié)器(finalizer)方法的對(duì)象的終結(jié)器隊(duì)列。OBJECTREF不是指向?qū)ο髮?shí)例的開始位置,而是有一個(gè)DWORD的偏移量(4字節(jié))。此DWORD稱為對(duì)象頭,保存一個(gè)指向SyncTableEntry表的索引(從1開始計(jì)數(shù)的syncblk編號(hào)。因?yàn)橥ㄟ^索引進(jìn)行連接,所以在需要增加表的大小時(shí),CLR可以在內(nèi)存中移動(dòng)這個(gè)表。SyncTableEntry維護(hù)一個(gè)反向的弱引用,以便CLR可以跟蹤SyncBlock的所有權(quán)。弱引用讓GC可以在沒有其它強(qiáng)引用存在時(shí)回收對(duì)象。SyncTableEntry還保存了一個(gè)指向SyncBlock的指針,包含了很少需要被一個(gè)對(duì)象的所有實(shí)例使用的有用的信息。這些信息包括對(duì)象鎖,哈希編碼,任何轉(zhuǎn)換層(thunking)數(shù)據(jù)和應(yīng)用程序域的索引。對(duì)于大多數(shù)的對(duì)象實(shí)例,不會(huì)為實(shí)際的SyncBlock分配內(nèi)存,而且syncblk編號(hào)為0。這一點(diǎn)在執(zhí)行線程遇到如lock(obj)或者obj.GetHashCode的語(yǔ)句時(shí)會(huì)發(fā)生變化,如下所示:

SmallClass obj = new SmallClass() // Do some work here lock(obj) { /* Do some synchronized work here */ } obj.GetHashCode();

在以上代碼中,smallObj會(huì)使用0作為它的起始的syncblk編號(hào)。lock語(yǔ)句使得CLR創(chuàng)建一個(gè)syncblk入口并使用相應(yīng)的數(shù)值更新對(duì)象頭。因?yàn)镃#的lock關(guān)鍵字會(huì)擴(kuò)展為try-finally語(yǔ)句并使用Monitor類,一個(gè)用作同步的Monitor對(duì)象在syncblk上創(chuàng)建。堆GetHashCode的調(diào)用會(huì)使用對(duì)象的哈希編碼增加syncblk。

在SyncBlock中有其它的域,它們?cè)贑OM交互操作和封送委托(marshaling delegates)到非托管代碼時(shí)使用,不過這和典型的對(duì)象用處無(wú)關(guān)。

類型句柄緊跟在對(duì)象實(shí)例中的syncblk編號(hào)后。為了保持連續(xù)性,我會(huì)在說(shuō)明實(shí)例變量后討論類型句柄。實(shí)例域(Instance field)的變量列表緊跟在類型句柄后。默認(rèn)情況下,實(shí)例域會(huì)以內(nèi)存最有效使用的方式排列,這樣只需要最少的用作對(duì)齊的填充字節(jié)。圖7的代碼顯示了SimpleClass包含有一些不同大小的實(shí)例變量。

圖8顯示了在Visual Studio調(diào)試器的內(nèi)存窗口中的一個(gè)SimpleClass對(duì)象實(shí)例。我們?cè)趫D7的return語(yǔ)句處設(shè)置了斷點(diǎn),然后使用ECX寄存器保存的simpleObj地址在內(nèi)存窗口顯示對(duì)象實(shí)例。前4個(gè)字節(jié)是syncblk編號(hào)。因?yàn)槲覀儧]有用任何同步代碼使用此實(shí)例(也沒有訪問它的哈希編碼),syncblk編號(hào)為0。保存在棧變量的對(duì)象實(shí)例,指向起始位置的4個(gè)字節(jié)的偏移處。字節(jié)變量b1,b2,b3和b4被一個(gè)接一個(gè)的排列在一起。兩個(gè)short類型變量s1和s2也被排列在一起。字符串變量str是一個(gè)4字節(jié)的OBJECTREF,指向GC堆中分配的實(shí)際的字符串實(shí)例。字符串是一個(gè)特別的類型,因?yàn)樗邪瑯游淖址?hào)的字符串,會(huì)在程序集加載到進(jìn)程時(shí)指向一個(gè)全局字符串表的同一實(shí)例。這個(gè)過程稱為字符串駐留(string interning),設(shè)計(jì)目的是優(yōu)化內(nèi)存的使用。我們之前已經(jīng)提過,在NET Framework 1.1中,程序集不能選擇是否使用這個(gè)過程,盡管未來(lái)版本的CLR可能會(huì)提供這樣的能力。

所以默認(rèn)情況下,成員變量在源代碼中的詞典順序沒有在內(nèi)存中保持。在交互操作的情況下,詞典順序必須被保存到內(nèi)存中,這時(shí)可以使用StructLayoutAttribute特性,它有一個(gè)LayoutKind的枚舉類型作為參數(shù)。LayoutKind.Sequential可以為被封送(marshaled)數(shù)據(jù)保持詞典順序,盡管在.NET Framework 1.1中,它沒有影響托管的布局(但是.NET Framework 2.0可能會(huì)這么做)。在交互操作的情況下,如果你確實(shí)需要額外的填充字節(jié)和顯示的控制域的順序,LayoutKind.Explicit可以和域?qū)哟蔚腇ieldOffset特性一起使用。

看完底層的內(nèi)存內(nèi)容后,我們使用SOS看看對(duì)象實(shí)例。一個(gè)有用的命令是DumpHeap,它可以列出所有的堆內(nèi)容和一個(gè)特別類型的所有實(shí)例。無(wú)需依賴寄存器,DumpHeap可以顯示我們創(chuàng)建的唯一一個(gè)實(shí)例的地址。

!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 Name955124 1 36 SimpleClass

對(duì)象的總大小是36字節(jié),不管字符串多大,SimpleClass的實(shí)例只包含一個(gè)DWORD的對(duì)象引用。SimpleClass的實(shí)例變量只占用28字節(jié),其它8個(gè)字節(jié)包括類型句柄(4字節(jié))和syncblk編號(hào)(4字節(jié))。找到simpleObj實(shí)例的地址后,我們可以使用DumpObj命令輸出它的內(nèi)容,如下所示:

!DumpObj 0x00a8197c Name: SimpleClass MethodTable 0x00955124 EEClass 0x02ca33b0 Size 36(0x24) bytes FieldDesc*: 00955064MT 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

正如之前說(shuō)過,C#編譯器對(duì)于類的默認(rèn)布局使用LayoutType.Auto(對(duì)于結(jié)構(gòu)使用LayoutType.Sequential);因此類加載器重新排列實(shí)例域以最小化填充字節(jié)。我們可以使用ObjSize來(lái)輸出包含被str實(shí)例占用的空間,如下所示:

!ObjSize 0x00a8197c sizeof(00a8197c) = 72 ( 0x48) bytes (SimpleClass)

如果你從對(duì)象圖的全局大小(72字節(jié))減去SimpleClass的大小(36字節(jié)),就可以得到str的大小,即36字節(jié)。讓我們輸出str實(shí)例來(lái)驗(yàn)證這個(gè)結(jié)果:

!DumpObj 0x00a819a0 Name: System.String MethodTable 0x009742d8 EEClass 0x02c4c6c4 Size 36(0x24) bytes

如果你將字符串實(shí)例的大小(36字節(jié))加上SimpleClass實(shí)例的大小(36字節(jié)),就可以得到ObjSize命令報(bào)告的總大小72字節(jié)。

請(qǐng)注意ObjSize不包含syncblk結(jié)構(gòu)占用的內(nèi)存。而且,在.NET Framework 1.1中,CLR不知道非托管資源占用的內(nèi)存,如GDI對(duì)象,COM對(duì)象,文件句柄等等;因此它們不會(huì)被這個(gè)命令報(bào)告。

指向方法表的類型句柄在syncblk編號(hào)后分配。在對(duì)象實(shí)例創(chuàng)建前,CLR查看加載類型,如果沒有找到,則進(jìn)行加載,獲得方法表地址,創(chuàng)建對(duì)象實(shí)例,然后把類型句柄值追加到對(duì)象實(shí)例中。JIT編譯器產(chǎn)生的代碼在進(jìn)行方法分派時(shí)使用類型句柄來(lái)定位方法表。CLR在需要史可以通過方法表反向訪問加載類型時(shí)使用類型句柄。

返回頁(yè)首

方法表

每個(gè)類和實(shí)例在加載到應(yīng)用程序域時(shí),會(huì)在內(nèi)存中通過方法表來(lái)表示。這是在對(duì)象的第一個(gè)實(shí)例創(chuàng)建前的類加載活動(dòng)的結(jié)果。對(duì)象實(shí)例表示的是狀態(tài),而方法表表示了行為。通過EEClass,方法表把對(duì)象實(shí)例綁定到被語(yǔ)言編譯器產(chǎn)生的映射到內(nèi)存的元數(shù)據(jù)結(jié)構(gòu)(metadata structures)。方法表包含的信息和外掛的信息可以通過System.Type訪問。指向方法表的指針在托管代碼中可以通過Type.RuntimeTypeHandle屬性獲得。對(duì)象實(shí)例包含的類型句柄指向方法表開始位置的偏移處,偏移量默認(rèn)情況下是12字節(jié),包含了GC信息。我們不打算在這里對(duì)其進(jìn)行討論。

圖9顯示了方法表的典型布局。我們會(huì)說(shuō)明類型句柄的一些重要的域,但是對(duì)于完全的列表,請(qǐng)參看此圖。讓我們從基實(shí)例大小(Base Instance Size)開始,因?yàn)樗苯雨P(guān)系到運(yùn)行時(shí)的內(nèi)存狀態(tài)。

返回頁(yè)首

基實(shí)例大小

基實(shí)例大小是由類加載器計(jì)算的對(duì)象的大小,基于代碼中聲明的域。之前已經(jīng)討論過,當(dāng)前GC的實(shí)現(xiàn)需要一個(gè)最少12字節(jié)的對(duì)象實(shí)例。如果一個(gè)類沒有定義任何實(shí)例域,它至少包含額外的4個(gè)字節(jié)。其它的8個(gè)字節(jié)被對(duì)象頭(可能包含syncblk編號(hào))和類型句柄占用。再說(shuō)一次,對(duì)象的大小會(huì)受到StructLayoutAttribute的影響。

看看圖3中顯示的MyClass(有兩個(gè)接口)的方法表的內(nèi)存快照(Visual Studio .NET 2003內(nèi)存窗口),將它和SOS的輸出進(jìn)行比較。在圖9中,對(duì)象大小位于4字節(jié)的偏移處,值為12(0x0000000C)字節(jié)。以下是SOS的DumpHeap命令的輸出:

!DumpHeap -type MyClassAddress MT Size 00a819ac 009552a0 12 total 1 objects Statistics:MT Count TotalSize Class Name 9552a0 1 12 MyClass 返回頁(yè)首

方法槽表(Method Slot Table)

在方法表中包含了一個(gè)槽表,指向各個(gè)方法的描述(MethodDesc),提供了類型的行為能力。方法槽表是基于方法實(shí)現(xiàn)的線性鏈表,按照如下順序排列:繼承的虛方法,引入的虛方法,實(shí)例方法,靜態(tài)方法。

類加載器在當(dāng)前類,父類和接口的元數(shù)據(jù)中遍歷,然后創(chuàng)建方法表。在排列過程中,它替換所有的被覆蓋的虛方法和被隱藏的父類方法,創(chuàng)建新的槽,在需要時(shí)復(fù)制槽。槽復(fù)制是必需的,它可以讓每個(gè)接口有自己的最小的vtable。但是被復(fù)制的槽指向相同的物理實(shí)現(xiàn)。MyClass包含接口方法,一個(gè)類構(gòu)造函數(shù)(.cctor)和對(duì)象構(gòu)造函數(shù)(.ctor)。對(duì)象構(gòu)造函數(shù)由C#編譯器為所有沒有顯式定義構(gòu)造函數(shù)的對(duì)象自動(dòng)生成。因?yàn)槲覀兌x并初始化了一個(gè)靜態(tài)變量,編譯器會(huì)生成一個(gè)類構(gòu)造函數(shù)。圖10顯示了MyClass的方法表的布局。布局顯示了10個(gè)方法,因?yàn)镸ethod2槽為接口IVMap進(jìn)行了復(fù)制,下面我們會(huì)進(jìn)行討論。圖11顯示了MyClass的方法表的SOS的輸出。

任何類型的開始4個(gè)方法總是ToString, Equals, GetHashCode, and Finalize。這些是從System.Object繼承的虛方法。Method2槽被進(jìn)行了復(fù)制,但是都指向相同的方法描述。代碼顯示定義的.cctor和.ctor會(huì)分別和靜態(tài)方法和實(shí)例方法分在一組。

返回頁(yè)首

方法描述(MethodDesc)

方法描述(MethodDesc)是CLR知道的方法實(shí)現(xiàn)的一個(gè)封裝。有幾種類型的方法描述,除了用于托管實(shí)現(xiàn),分別用于不同的交互操作實(shí)現(xiàn)的調(diào)用。在本文中,我們只考察圖3代碼中的托管方法描述。方法描述在類加載過程中產(chǎn)生,初始化為指向IL。每個(gè)方法描述帶有一個(gè)預(yù)編譯代理(PreJitStub),負(fù)責(zé)觸發(fā)JIT編譯。圖12顯示了一個(gè)典型的布局,方法表的槽實(shí)際上指向代理,而不是實(shí)際的方法描述數(shù)據(jù)結(jié)構(gòu)。對(duì)于實(shí)際的方法描述,這是-5字節(jié)的偏移,是每個(gè)方法的8個(gè)附加字節(jié)的一部分。這5個(gè)字節(jié)包含了調(diào)用預(yù)編譯代理程序的指令。5字節(jié)的偏移可以從SOS的DumpMT輸出從看到,因?yàn)榉椒枋隹偸欠椒ú郾碇赶虻奈恢煤竺娴?個(gè)字節(jié)。在第一次調(diào)用時(shí),會(huì)調(diào)用JIT編譯程序。在編譯完成后,包含調(diào)用指令的5個(gè)字節(jié)會(huì)被跳轉(zhuǎn)到JIT編譯后的x86代碼的無(wú)條件跳轉(zhuǎn)指令覆蓋。


圖12 方法描述

對(duì)圖12的方法表槽指向的代碼進(jìn)行反匯編,顯示了對(duì)預(yù)編譯代理的調(diào)用。以下是在Method2被JIT編譯前的反匯編的簡(jiǎn)化顯示。

!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

現(xiàn)在我們執(zhí)行此方法,然后反匯編相同的地址:

!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

在此地址,只有開始5個(gè)字節(jié)是代碼,剩余字節(jié)包含了Method2的方法描述的數(shù)據(jù)。“!u”命令不知道這一點(diǎn),所以生成的是錯(cuò)亂的代碼,你可以忽略5個(gè)字節(jié)后的所有東西。

CodeOrIL在JIT編譯前包含IL中方法實(shí)現(xiàn)的相對(duì)虛地址(Relative Virtual Address ,RVA)。此域用作標(biāo)志,表示是否IL。在按要求編譯后,CLR使用編譯后的代碼地址更新此域。讓我們從列出的函數(shù)中選擇一個(gè),然后用DumpMT命令分別輸出在JIT編譯前后的方法描述的內(nèi)容:

!DumpMD 0x00955268 Method Name : [DEFAULT] [hasThis] Void MyClass.Method2() MethodTable 9552a0 Module: 164008 mdToken: 06000006 Flags : 400 IL RVA : 00002068

編譯后,方法描述的內(nèi)容如下:

!DumpMD 0x00955268 Method Name : [DEFAULT] [hasThis] Void MyClass.Method2() MethodTable 9552a0 Module: 164008 mdToken: 06000006 Flags : 400 Method VA : 02c633e8

方法的這個(gè)標(biāo)志域的編碼包含了方法的類型,例如靜態(tài),實(shí)例,接口方法或者COM實(shí)現(xiàn)。讓我們看方法表另外一個(gè)復(fù)雜的方面:接口實(shí)現(xiàn)。它封裝了布局過程所有的復(fù)雜性,讓托管環(huán)境覺得這一點(diǎn)看起來(lái)簡(jiǎn)單。然后,我們將說(shuō)明接口如何進(jìn)行布局和基于接口的方法分派的確切工作方式。

返回頁(yè)首

接口虛表圖和接口圖

在方法表的第12字節(jié)偏移處是一個(gè)重要的指針,接口虛表(IVMap)。如圖9所示,接口虛表指向一個(gè)應(yīng)用程序域?qū)哟蔚挠成浔?#xff0c;該表以進(jìn)程層次的接口ID作為索引。接口ID在接口類型第一次加載時(shí)創(chuàng)建。每個(gè)接口的實(shí)現(xiàn)都在接口虛表中有一個(gè)記錄。如果MyInterface1被兩個(gè)類實(shí)現(xiàn),在接口虛表表中就有兩個(gè)記錄。該記錄會(huì)反向指向MyClass方法表內(nèi)含的子表的開始位置,如圖9所示。這是接口方法分派發(fā)生時(shí)使用的引用。接口虛表是基于方法表內(nèi)含的接口圖信息創(chuàng)建,接口圖在方法表布局過程中基于類的元數(shù)據(jù)創(chuàng)建。一旦類型加載完成,只有接口虛表用于方法分派。

第28字節(jié)位置的接口圖會(huì)指向內(nèi)含在方法表中的接口信息記錄。在這種情況下,對(duì)MyClass實(shí)現(xiàn)的兩個(gè)接口中的每一個(gè)都有兩條記錄。第一條接口信息記錄的開始4個(gè)字節(jié)指向MyInterface1的類型句柄(見圖9和圖10)。接著的WORD(2字節(jié))被一個(gè)標(biāo)志占用(0表示從父類派生,1表示由當(dāng)前類實(shí)現(xiàn))。在標(biāo)志后的WORD是一個(gè)開始槽(Start Slot),被類加載器用來(lái)布局接口實(shí)現(xiàn)的子表。對(duì)于MyInterface2,開始槽的值為4(從0開始編號(hào)),所以槽5和6指向?qū)崿F(xiàn);對(duì)于MyInterface2,開始槽的值為6,所以槽7和8指向?qū)崿F(xiàn)。類加載器會(huì)在需要時(shí)復(fù)制槽來(lái)產(chǎn)生這樣的效果:每個(gè)接口有自己的實(shí)現(xiàn),然而物理映射到同樣的方法描述。在MyClass中,MyInterface1.Method2和MyInterface2.Method2會(huì)指向相同的實(shí)現(xiàn)。

基于接口的方法分派通過接口虛表進(jìn)行,而直接的方法分派通過保存在各個(gè)槽的方法描述地址進(jìn)行。如之前提及,.NET框架使用fastcall的調(diào)用約定,最先2個(gè)參數(shù)在可能的時(shí)候一般通過ECX和EDX寄存器傳遞。實(shí)例方法的第一個(gè)參數(shù)總是this指針,所以通過ECX寄存器傳送,可以在“mov ecx,esi”語(yǔ)句看到這一點(diǎn):

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

這些反匯編顯示了直接調(diào)用MyClass的實(shí)例方法沒有使用偏移。JIT編譯器把方法描述的地址直接寫到代碼中。基于接口的分派通過接口虛表發(fā)生,和直接分派相比需要一些額外的指令。一個(gè)指令用來(lái)獲得接口虛表的地址,另一個(gè)獲取方法槽表中的接口實(shí)現(xiàn)的開始槽。而且,把一個(gè)對(duì)象實(shí)例轉(zhuǎn)換為接口只需要拷貝this指針到目標(biāo)的變量。在圖2中,語(yǔ)句“mi1=mc”使用一個(gè)指令把mc的對(duì)象引用拷貝到mi1。

返回頁(yè)首

虛分派(Virtual Dispatch)

現(xiàn)在我們看看虛分派,并且和基于接口的分派進(jìn)行比較。以下是圖3中MyClass.Method3的虛函數(shù)調(diào)用的反匯編代碼:

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

虛分派總是通過一個(gè)固定的槽編號(hào)發(fā)生,和方法表指針在特定的類(類型)實(shí)現(xiàn)層次無(wú)關(guān)。在方法表布局時(shí),類加載器用覆蓋的子類的實(shí)現(xiàn)代替父類的實(shí)現(xiàn)。結(jié)果,對(duì)父對(duì)象的方法調(diào)用被分派到子對(duì)象的實(shí)現(xiàn)。反匯編顯示了分派通過8號(hào)槽發(fā)生,可以在調(diào)試器的內(nèi)存窗口(如圖10所示)和DumpMT的輸出看到這一點(diǎn)。

返回頁(yè)首

靜態(tài)變量

靜態(tài)變量是方法表數(shù)據(jù)結(jié)構(gòu)的重要組成部分。作為方法表的一部分,它們分配在方法表的槽數(shù)組后。所有的原始靜態(tài)類型是內(nèi)聯(lián)的,而對(duì)于結(jié)構(gòu)和引用的類型的靜態(tài)值對(duì)象,通在句柄表中創(chuàng)建的對(duì)象引用來(lái)指向。方法表中的對(duì)象引用指向應(yīng)用程序域的句柄表的對(duì)象引用,它引用了堆上創(chuàng)建的對(duì)象實(shí)例。一旦創(chuàng)建后,句柄表內(nèi)的對(duì)象引用會(huì)使堆上的對(duì)象實(shí)例保持生存,直到應(yīng)用程序域被卸載。在圖9 中,靜態(tài)字符串變量str指向句柄表的對(duì)象引用,后者指向GC堆上的MyString。

返回頁(yè)首

EEClass

EEClass在方法表創(chuàng)建前開始生存,它和方法表結(jié)合起來(lái),是類型聲明的CLR版本。實(shí)際上,EEClass和方法表邏輯上是一個(gè)數(shù)據(jù)結(jié)構(gòu)(它們一起表示一個(gè)類型),只不過因?yàn)槭褂妙l度的不同而被分開。經(jīng)常使用的域放在方法表,而不經(jīng)常使用的域在EEClass中。這樣,需要被JIT編譯函數(shù)使用的信息(如名字,域和偏移)在EEClass中,但是運(yùn)行時(shí)需要的信息(如虛表槽和GC信息)在方法表中。

對(duì)每一個(gè)類型會(huì)加載一個(gè)EEClass到應(yīng)用程序域中,包括接口,類,抽象類,數(shù)組和結(jié)構(gòu)。每個(gè)EEClass是一個(gè)被執(zhí)行引擎跟蹤的樹的節(jié)點(diǎn)。CLR使用這個(gè)網(wǎng)絡(luò)在EEClass結(jié)構(gòu)中瀏覽,其目的包括類加載,方法表布局,類型驗(yàn)證和類型轉(zhuǎn)換。EEClass的子-父關(guān)系基于繼承層次建立,而父-子關(guān)系基于接口層次和類加載順序的結(jié)合。在執(zhí)行托管代碼的過程中,新的EEClass節(jié)點(diǎn)被加入,節(jié)點(diǎn)的關(guān)系被補(bǔ)充,新的關(guān)系被建立。在網(wǎng)絡(luò)中,相鄰的EEClass還有一個(gè)水平的關(guān)系。EEClass有三個(gè)域用于管理被加載類型的節(jié)點(diǎn)關(guān)系:父類(Parent Class),相鄰鏈(sibling chain)和子鏈(children chain)。關(guān)于圖4中的MyClass上下文中的EEClass的語(yǔ)義,請(qǐng)參考圖13。

圖13只顯示了和這個(gè)討論相關(guān)的一些域。因?yàn)槲覀兒雎粤瞬季种械囊恍┯?#xff0c;我們沒有在圖中確切顯示偏移。EEClass有一個(gè)間接的對(duì)于方法表的引用。EEClass也指向在默認(rèn)應(yīng)用程序域的高頻堆分配的方法描述塊。在方法表創(chuàng)建時(shí),對(duì)進(jìn)程堆上分配的域描述列表的一個(gè)引用提供了域的布局信息。EEClass在應(yīng)用程序域的低頻堆分配,這樣操作系統(tǒng)可以更好的進(jìn)行內(nèi)存分頁(yè)管理,因此減少了工作集。


圖13 EEClass 布局

圖13中的其它域在MyClass(圖3)的上下文的意義不言自明。我們現(xiàn)在看看使用SOS輸出的EEClass的真正的物理內(nèi)存。在mc.Method1代碼行設(shè)置斷點(diǎn)后,運(yùn)行圖3的程序。首先使用命令Name2EE獲得MyClass的EEClass的地址。

!Name2EE C:\Working\test\ClrInternals\Sample1.exe MyClass MethodTable: 009552a0 EEClass: 02ca3508 Name: MyClass

Name2EE的第一個(gè)參數(shù)時(shí)模塊名,可以從DumpDomain命令得到。現(xiàn)在我們得到了EEClass的地址,我們輸出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*: 00955224MT Field Offset Type Attr Value Name 009552a0 4000001 2c CLASS static 00a8198c str 009552a0 4000002 30 System.UInt32 static aaaaaaaa ui

圖13和DumpClass的輸出看起來(lái)完全一樣。元數(shù)據(jù)令牌(metadata token,mdToken)表示了在模塊PE文件中映射到內(nèi)存的元數(shù)據(jù)表的MyClass索引,父類指向System.Object。從相鄰鏈指向名為Program的EEClass,可以知道圖13顯示的是加載Program時(shí)的結(jié)果。

MyClass有8個(gè)虛表槽(可以被虛分派的方法)。即使Method1和Method2不是虛方法,它們可以在通過接口進(jìn)行分派時(shí)被認(rèn)為是虛函數(shù)并加入到列表中。把.cctor和.ctor加入到列表中,你會(huì)得到總共10個(gè)方法。最后列出的是類的兩個(gè)靜態(tài)域。MyClass沒有實(shí)例域。其它域不言自明。

返回頁(yè)首

Conclusion結(jié)論

我們關(guān)于CLR一些最重要的內(nèi)在的探索旅程終于結(jié)束了。顯然,還有許多問題需要涉及,而且需要在更深的層次上討論,但是我們希望這可以幫助你看到事物如何工作。這里提供的許多的信息可能會(huì)在.NET框架和CLR的后來(lái)版本中改變,不過盡管本文提到的CLR數(shù)據(jù)結(jié)構(gòu)可能改變,概念應(yīng)該保持不變。

Hanu Kommalapati是微軟Gulf Coast區(qū)(休斯頓)的一名架構(gòu)師。他在微軟現(xiàn)在的角色是幫助客戶基于.NET框架建立可擴(kuò)展的組件框架。可以通過hanuk@microsoft.com聯(lián)系他。

Tom Christian是微軟開發(fā)支持高級(jí)工程師,使用ASP.NET和用于WinDBG的.NET調(diào)試器擴(kuò)展(sos/ psscor)。他在北卡羅來(lái)州的夏洛特,可以通過tomchris@microsoft.com聯(lián)系他。

翻譯者Luke是微軟公司的軟件工程師,習(xí)慣使用C++和C#開發(fā)應(yīng)用程序。閑暇時(shí)間他喜歡音樂,旅游和懷舊游戲,并且愿意幫助MSDN翻譯更多的文章和其他開發(fā)者共享。可以通過ecaijw@msn.com聯(lián)系他。

轉(zhuǎn)載于:https://my.oschina.net/ind/blog/299206

總結(jié)

以上是生活随笔為你收集整理的深入探索.NET框架内部了解CLR如何创建运行时对象的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。