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