C#內(nèi)存管理CLR深入講解(上篇)
半年之前,PM讓我在部門(mén)內(nèi)部進(jìn)行一次關(guān)于“內(nèi)存泄露”的專(zhuān)題分享,我為此準(zhǔn)備了一份PPT。今天無(wú)意中將其翻出來(lái),覺(jué)得里面提到的關(guān)于CLR下關(guān)于內(nèi)存管理部分的內(nèi)存還有點(diǎn)意思。為此,今天按照PPT的內(nèi)容寫(xiě)了一篇文章。本篇文章不會(huì)在討論那些我們熟悉的話題,比如“值類(lèi)型引用類(lèi)型具有怎樣的區(qū)別?”、“垃圾回收分為幾個(gè)步驟?”、“Finalizer和Dispose有何不同”、等等,而是討論一些不同的內(nèi)容。整篇文章分上下兩篇,上篇主要談?wù)摰氖?ldquo;程序集(Assembly)和應(yīng)用程序域(AppDomain)”。也許有的地方說(shuō)的不是很正確,希望讀者不吝賜教。
一、程序集與應(yīng)用程序域
何謂程序集(Assembly)?它是一個(gè)托管應(yīng)用的基本的部署單元。一個(gè)程序集是自描述的(通過(guò)元數(shù)據(jù))、能夠?qū)嵤┌姹静呗院筒渴鸩呗?。我傾向于這樣的方式來(lái)定義程序集:“Assembly is a reusable, versionable, and self-describing building block of a CLR application.”從結(jié)構(gòu)組成來(lái)看,一個(gè)程序集主要由三個(gè)部署組成:IL指令、元數(shù)據(jù)和資源。程序集的結(jié)構(gòu)組成如下圖所示。
那么什么又是應(yīng)用程序域呢?從功能上講,通過(guò)應(yīng)用程序域?qū)崿F(xiàn)的隔離機(jī)制為托管代碼的執(zhí)行提供了一個(gè)安全的邊界。從與程序集的關(guān)系來(lái)講,我們可以將應(yīng)用程序域看成是加載程序集的容器。只有相關(guān)的程序集被CLR加載到相應(yīng)的應(yīng)用程序域中,才談得上代碼的執(zhí)行。
基于應(yīng)用程序域的隔離,歸根結(jié)底是內(nèi)存的隔離。一個(gè)基本的反映就是:在一個(gè)應(yīng)用程序域中創(chuàng)建的對(duì)象,不能直接在另一個(gè)應(yīng)用程序域中使用。這中間需要有一個(gè)基本的跨應(yīng)用程序域傳遞的機(jī)制,我們將這種機(jī)制稱(chēng)之為“封送(Marshaling)”。具體來(lái)講,又具有兩種不同的封送方式:按值封送(MBV:Marshaling By Value )和按引用封送(MBR:Marshaling By Reference)。MBV主要采用序列化的方式,而MBR最典型的就是.ENT Remoting。
二、系統(tǒng)程序域、共享程序域和默認(rèn)程序域
當(dāng)托管應(yīng)用被啟動(dòng)后,在執(zhí)行第一句代碼之前,CLR會(huì)先后為我們創(chuàng)建三個(gè)應(yīng)用程序域:系統(tǒng)程序域(System Domain)、共享程序域(Shared Domain)和默認(rèn)程序域(Default Domain),它們分別具有不同的作用。
- 系統(tǒng)程序域:系統(tǒng)程序域是第一個(gè)被創(chuàng)建的應(yīng)用程序域,同時(shí)也是其他兩個(gè)應(yīng)用程序域的創(chuàng)建者。在該程序域初始化過(guò)程中,由它將msCorLib.dll這個(gè)程序集(這是一個(gè)很重要的程序集,.NET類(lèi)型系統(tǒng)最基本的類(lèi)型定義其中)加載到共享程序域中。此外,駐留的字符串也被保存在此系統(tǒng)程序域中。系統(tǒng)程序域的一個(gè)主要的任務(wù)是追蹤其他所有應(yīng)用程序域的狀態(tài),并負(fù)責(zé)加載和卸載它們;
- 共享程序域:共享程序域主要用于保存以“中立域(Domain-neutral Domain )”加載的程序集容器。所謂“中立域 ”方式加載的程序集,就是說(shuō)程序集并不被加載到當(dāng)前的程序域中并被該程序域?qū)S?,而是加載到一個(gè)公共的程序域中被所有程序域共享。
- 默認(rèn)程序域:我們的托管程序最終就運(yùn)行在該程序域中,默認(rèn)程序域可以通過(guò)System.AppDomain表示。
三、字符串的駐留
上面的文字描述實(shí)際上透露一些重要的信息,其中一個(gè)就是字符串的駐留(String Interning)。關(guān)于字符串的駐留,我想大家都不陌生,所以在這里我就不作重復(fù)的介紹了。在這里,我只想討論一個(gè)問(wèn)題:字符串的駐留是基于整個(gè)進(jìn)程的,而不是僅僅基于某個(gè)應(yīng)用程序域。
從上面的描述我們知道,字符串對(duì)象和一般的引用類(lèi)型對(duì)象具有很大的不同:字符串對(duì)象直接被保存到系統(tǒng)程序域中,而一般的引用類(lèi)型對(duì)象我們都是最終保存在GC堆中。從某種意義上講,在字符串駐留機(jī)制下,字符串也是以“中立域”的方式被加載的,被駐留的字符串能夠被同一個(gè)進(jìn)程下所有應(yīng)用程序域所共享。
那么,我們是否可以通過(guò)一些比較直觀的方式來(lái)驗(yàn)證這一點(diǎn)。但是,我們不能直接編寫(xiě)程序來(lái)比較兩個(gè)應(yīng)用程序域中字符串是否是相同的引用,但是我們有一些間接的機(jī)制。我個(gè)人喜歡采用的方式是:加鎖。我們?cè)谶\(yùn)行于不同的應(yīng)用程序域的代碼中對(duì)兩個(gè)字符串變量進(jìn)行加鎖,如果程序運(yùn)行的結(jié)果和對(duì)相同的對(duì)象加鎖一樣,那么就可以證明被枷鎖的兩個(gè)對(duì)象實(shí)際上是同一個(gè)對(duì)象。
為了便于演示,我寫(xiě)一個(gè)如下一個(gè)AppDomainContext,表示某個(gè)AppDomain對(duì)應(yīng)的執(zhí)行上下文。AppDomainContext具有一個(gè)只讀的類(lèi)型為AppDomain的屬性,該屬性通過(guò)構(gòu)造函數(shù)執(zhí)行,最終在靜態(tài)方法NewContext被創(chuàng)建。我們調(diào)用Invoke方法讓指定的方法對(duì)應(yīng)的應(yīng)用程序域中執(zhí)行。
public class AppDomainContext { public AppDomain AppDomain { get; private set; } private AppDomainContext(AppDomain appDomain) { this.AppDomain = appDomain; } public static AppDomainContext NewContext(string friendlyName) { return new AppDomainContext(AppDomain.CreateDomain(friendlyName)); } public void Invoke<T>(Action<T> action) where T : MarshalByRefObject { T instance = (T)this.AppDomain.CreateInstanceAndUnwrap(typeof(T).Assembly.FullName, typeof(T).FullName); action.Invoke(instance); } }
我們接著在定義一個(gè)輔助類(lèi)ObjectLock方便進(jìn)行加鎖,以及確認(rèn)對(duì)象是否被所住。ObjectLock比如繼承自MarshalByRefObject,因?yàn)槲覀冃枰搶?duì)象以MBR的方式進(jìn)行傳遞。在Lock方法中對(duì)指定的對(duì)象進(jìn)行加鎖,并指定加鎖的時(shí)間。在CheckLock中通過(guò)時(shí)間間隔判斷指定的對(duì)象是否已經(jīng)被鎖住,相應(yīng)的結(jié)果會(huì)在控制臺(tái)中被輸出。為了讓大家能夠確定相應(yīng)的操作是在哪個(gè)應(yīng)用程序域中執(zhí)行的,在枷鎖和檢查鎖定的時(shí)候?qū)?yīng)用程序域的名稱(chēng)(AppDomain.FriendlyName屬性)打印出來(lái)。
public class ObjectLock : MarshalByRefObject { public void Lock(object objectToLock, int millisecondsTimeout) { lock (objectToLock) { Console.WriteLine("[{0}] Successfully lock the object.", AppDomain.CurrentDomain.FriendlyName); Thread.Sleep(millisecondsTimeout); } } public void CheckLock(object objectToLock) { if (Monitor.TryEnter(objectToLock, 10)) { Console.WriteLine("[{0}] The object is not locked.", AppDomain.CurrentDomain.FriendlyName); } else { Console.WriteLine("[{0}] The object is locked .", AppDomain.CurrentDomain.FriendlyName); } } }
然后我再一個(gè)控制臺(tái)應(yīng)用中的Main方法中,編寫(xiě)了如下簡(jiǎn)單的代碼。通過(guò)AppDomainContext在一個(gè)的應(yīng)用程序域(Foo)中鎖定一個(gè)值為“Hello World!”的字符串,并在另一個(gè)應(yīng)用程序域(Bar)中確認(rèn)同值得字符串是否已經(jīng)被鎖定。結(jié)果表示在應(yīng)用程序域Bar中指定的字符串已經(jīng)被鎖定,從而證明了應(yīng)用程序域Foo和Bar中兩個(gè)值為“Hello World!”的字符串對(duì)象實(shí)際上是同一個(gè)。
static void Main(string[] args) { Action<ObjectLock> lockObj = objLock => objLock.Lock("Hello World!", 2000); Action<ObjectLock> checkLock = objLock => objLock.CheckLock("Hello World!"); Thread lockObjThread = new Thread(() => AppDomainContext.NewContext("Foo").Invoke<ObjectLock>(lockObj)); Thread checkLockThread = new Thread(() => AppDomainContext.NewContext("Bar").Invoke<ObjectLock>(checkLock)); lockObjThread.Start(); Thread.Sleep(500); checkLockThread.Start(); }
輸出結(jié)果:
1: [Foo] Successfully lock the object. 2: [Bar] The object is locked.
上面的介紹同時(shí)說(shuō)明一個(gè)問(wèn)題:千萬(wàn)不要對(duì)一個(gè)字符串對(duì)象加鎖。
四、程序集加載的方式
雖然我們說(shuō)CLR在啟動(dòng)托管應(yīng)用的時(shí)候,以中立域的方式加載msCorLib.dll這個(gè)程序集,但是這不是程序集默認(rèn)采用的加載方式。在默認(rèn)的情況下,程序集被加載到當(dāng)前的程序域中,供該程序集獨(dú)占使用。我個(gè)人將這兩種不同的程序集加載方式稱(chēng)為:獨(dú)占加載(Exclusive Loading )和共享加載(Shared Loading)。如右圖所示:如果某個(gè)類(lèi)型被定義在程序集中Foo.Dll,當(dāng)AppDomain1和AppDomain2需要使用該類(lèi)型的時(shí)候,它們會(huì)分別以獨(dú)占的方式加載程序集Foo.Dll。但是,如果它們使用一些基元類(lèi)型,比如System.Object、System.Int32、System.DateTime等,則不會(huì)加載定義它們的msCorLib.dll程序集,而是直接使用已經(jīng)被以中立域方式加載到共享程序域中的msCorLib.dll。
我們同樣可以借助上面定義的AppDomainContext來(lái)證明這一點(diǎn)。在這之前我需要說(shuō)明一點(diǎn):程序集的加載包括對(duì)定義在程序集中類(lèi)型系統(tǒng)的加載,我們可以通過(guò)類(lèi)型對(duì)象的加鎖情況來(lái)推斷程序集的加載方式。為此我在上面創(chuàng)建的解決方案中添加了一個(gè)類(lèi)庫(kù)項(xiàng)目Lib,ConsoleApp引用Lib項(xiàng)目,并在Lib中定義了一個(gè)空的Foo類(lèi)型。
namespace Artech.MemAllocation { public class Foo {} }
然后我們修改之前的程序,將對(duì)字符串加鎖替換在對(duì)Foo類(lèi)型(typeof(Foo))加鎖。從輸出結(jié)果我們可以看出,在Bar程序域中使用的Foo類(lèi)型并沒(méi)有被鎖住,從而證明兩個(gè)程序域(Foo和Bar)使用的同一個(gè)類(lèi)型并不是Type對(duì)象,因?yàn)閷?duì)應(yīng)的程序集是以獨(dú)占的方式加載的。
static void Main(string[] args) { Action<ObjectLock> lockObj = objLock => objLock.Lock(typeof(Foo), 2000); Action<ObjectLock> checkLock = objLock => objLock.CheckLock(typeof(Foo)); Thread lockObjThread = new Thread(() => AppDomainContext.NewContext("Foo").Invoke<ObjectLock>(lockObj)); Thread checkLockThread = new Thread(() => AppDomainContext.NewContext("Bar").Invoke<ObjectLock>(checkLock)); lockObjThread.Start(); Thread.Sleep(500); checkLockThread.Start(); }
輸出結(jié)果:
[Foo] Successfully lock the object. [Bar] The object is not locked.
但是,如果我們將加鎖和鎖定檢驗(yàn)的typeof(Foo)替換成typeof(int),結(jié)果就完全不一樣了。不同的結(jié)果說(shuō)明了msCorLib.dll采用了不同于上面的程序集加載方式,以中立域方法的加載方式?jīng)Q定在任何應(yīng)用程序域中使用的類(lèi)型都是同一個(gè)Type對(duì)象。
static void Main(string[] args) { Action<ObjectLock> lockObj = objLock => objLock.Lock(typeof(int), 2000); Action<ObjectLock> checkLock = objLock => objLock.CheckLock(typeof(int)); Thread lockObjThread = new Thread(() => AppDomainContext.NewContext("Foo").Invoke<ObjectLock>(lockObj)); Thread checkLockThread = new Thread(() => AppDomainContext.NewContext("Bar").Invoke<ObjectLock>(checkLock)); lockObjThread.Start(); Thread.Sleep(500); checkLockThread.Start(); }
輸出結(jié)果:
[Foo] Successfully lock the object. [Bar] The object is locked.
五、我們自己的程序集也可以采用中立域的方式加載嗎?
我想到這里有人會(huì)問(wèn)一個(gè)問(wèn)題:“我們自定義的程序集可以像msCorLib.dll一樣以中立域的方式共享加載嗎?”。對(duì)于控制臺(tái)應(yīng)用,你只需要在Main方法上應(yīng)用LoaderOptimizationAttribute特性,并指定LoaderOptimization為MultiDomain即可。比如,還是采用對(duì)Foo類(lèi)型Foo類(lèi)型(typeof(Foo))對(duì)象加鎖,這次我們?cè)贛ain方法上應(yīng)用了這樣的特性:[LoaderOptimization(LoaderOptimization.MultiDomain)]。輸出的結(jié)果就與對(duì)Int32類(lèi)型對(duì)象加鎖一樣。
[LoaderOptimization(LoaderOptimization.MultiDomain)] static void Main(string[] args) { Action<ObjectLock> lockObj = objLock => objLock.Lock(typeof(Foo), 2000); Action<ObjectLock> checkLock = objLock => objLock.CheckLock(typeof(Foo)); Thread lockObjThread = new Thread(() => AppDomainContext.NewContext("Foo").Invoke<ObjectLock>(lockObj)); Thread checkLockThread = new Thread(() => AppDomainContext.NewContext("Bar").Invoke<ObjectLock>(checkLock)); lockObjThread.Start(); Thread.Sleep(500); checkLockThread.Start(); }
輸出結(jié)果:
[Foo] Successfully lock the object. [Bar] The object is locked.
又一個(gè)關(guān)于加鎖的注意:謹(jǐn)慎地對(duì)Type對(duì)象進(jìn)行加鎖。
關(guān)于CLR內(nèi)存管理一些深層次的討論[上篇]
關(guān)于CLR內(nèi)存管理一些深層次的討論[下篇]
到此這篇關(guān)于C#內(nèi)存管理CLR深入講解的文章就介紹到這了。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
C#正則表達(dá)式分解和轉(zhuǎn)換IP地址實(shí)例(C#正則表達(dá)式大全 c#正則表達(dá)式語(yǔ)法)
這是我發(fā)了不少時(shí)間整理的C#的正則表達(dá)式,新手朋友注意一定要手冊(cè)一下哦,這樣可以節(jié)省很多寫(xiě)代碼的時(shí)間。下面進(jìn)行了簡(jiǎn)單總結(jié)2013-12-12C#判斷指定驅(qū)動(dòng)器是否是Fat分區(qū)格式的方法
這篇文章主要介紹了C#判斷指定驅(qū)動(dòng)器是否是Fat分區(qū)格式的方法,涉及C#中DriveFormat屬性的使用技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04WPF實(shí)現(xiàn)動(dòng)畫(huà)效果
這篇文章介紹了WPF實(shí)現(xiàn)動(dòng)畫(huà)效果的方法,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-01-01C#使用WebClient登錄網(wǎng)站并抓取登錄后的網(wǎng)頁(yè)信息實(shí)現(xiàn)方法
這篇文章主要介紹了C#使用WebClient登錄網(wǎng)站并抓取登錄后的網(wǎng)頁(yè)信息實(shí)現(xiàn)方法,涉及C#基于會(huì)話操作登陸網(wǎng)頁(yè)及頁(yè)面讀取相關(guān)操作技巧,需要的朋友可以參考下2017-05-05C#代碼實(shí)現(xiàn)對(duì)AES加密解密
這篇文章主要介紹了C#代碼實(shí)現(xiàn)對(duì)AES加密解密的相關(guān)資料,AES是一個(gè)新的可以用于保護(hù)電子數(shù)據(jù)的加密算法,需要的朋友可以參考下2015-12-12C#面向?qū)ο笤O(shè)計(jì)原則之里氏替換原則
這篇文章介紹了C#面向?qū)ο笤O(shè)計(jì)原則之里氏替換原則,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-03-03