關於CLR內存管理一些深層次的討論[上篇]
半年之前,PM讓我在部門內部進行一次關於“內存泄露”的專題分享,我為此準備了一份PPT。今天無意中將其翻出來,覺得裏麵提到的關於CLR下關於內存管理部分的內存還有點意思。為此,今天按照PPT的內容寫了一篇文章。本篇文章不會在討論那些我們熟悉的話題,比如“值類型引用類型具有怎樣的區別?”、“垃圾回收分為幾個步驟?”、“Finalizer和Dispose有何不同”、等等,而是討論一些不同的內容。整篇文章分上下兩篇,上篇主要談論的是“程序集(Assembly)和應用程序域(AppDomain)”。也許有的地方說的不是很正確,希望讀者不吝賜教。
一、程序集與應用程序域
何謂程序集(Assembly)?它是一個托管應用的基本的部署單元。一個程序集是自描述的(通過元數據)、能夠實施版本策略和部署策略。我傾向於這樣的方式來定義程序集:“Assembly is a reusable, versionable, and self-describing building block of a CLR application.”從結構組成來看,一個程序集主要由三個部署組成:、和。程序集的結構組成如下圖所示。
那麼什麼又是應用程序域呢?從功能上講,通過應用程序域實現的隔離機製為托管代碼的執行提供了一個安全的邊界。從與程序集的關係來講,我們可以將應用程序域看成是。隻有相關的程序集被CLR加載到相應的應用程序域中,才談得上代碼的執行。
基於應用程序域的隔離,歸根結底是內存的隔離。一個基本的反映就是:在一個應用程序域中創建的對象,不能直接在另一個應用程序域中使用。這中間需要有一個基本的跨應用程序域傳遞的機製,我們將這種機製稱之為“”。具體來講,又具有兩種不同的封送方式:(MBV:Marshaling By Value )和(MBR:Marshaling By Reference)。MBV主要采用序列化的方式,而MBR最典型的就是.ENT Remoting。
二、係統程序域、共享程序域和默認程序域
當托管應用被啟動後,在執行第一句代碼之前,CLR會先後為我們創建三個應用程序域:係統程序域(System Domain)、共享程序域(Shared Domain)和默認程序域(Default Domain),它們分別具有不同的作用。
- 係統程序域:係統程序域是第一個被創建的應用程序域,同時也是其他兩個應用程序域的創建者。在該程序域初始化過程中,由它將這個程序集(這是一個很重要的程序集,.NET類型係統最基本的類型定義其中)加載到中。此外,也被保存在此係統程序域中。係統程序域的一個主要的任務是追蹤其他所有應用程序域的狀態,並負責加載和卸載它們;
- 共享程序域:共享程序域主要用於保存以“”加載的程序集容器。所謂“中立域 ”方式加載的程序集,就是說程序集並不被加載到當前的程序域中並被該程序域專用,而是加載到一個公共的程序域中被所有程序域共享。
- 默認程序域:我們的托管程序最終就運行在該程序域中,默認程序域可以通過System.AppDomain表示。
三、字符串的駐留
上麵的文字描述實際上透露一些重要的信息,其中一個就是(String Interning)。關於字符串的駐留,我想大家都不陌生,所以在這裏我就不作重複的介紹了。在這裏,我隻想討論一個問題:基於整個進程的,不是僅僅基於某個應用程序域。
從上麵的描述我們知道,字符串對象和一般的引用類型對象具有很大的不同:字符串對象直接被保存到係統程序域中,而一般的引用類型對象我們都是最終保存在GC堆中。從某種意義上講,在字符串駐留機製下,,被駐留的字符串能夠被同一個進程下所有應用程序域所共享。
那麼,我們是否可以通過一些比較直觀的方式來驗證這一點。但是,我們不能直接編寫程序來比較兩個應用程序域中字符串是否是相同的引用,但是我們有一些間接的機製。我個人喜歡采用的方式是:。我們在運行於不同的應用程序域的代碼中對兩個字符串變量進行加鎖,如果程序運行的結果和對相同的對象加鎖一樣,那麼就可以證明被枷鎖的兩個對象實際上是同一個對象。
為了便於演示,我寫一個如下一個AppDomainContext,表示某個AppDomain對應的執行上下文。AppDomainContext具有一個隻讀的類型為AppDomain的屬性,該屬性通過構造函數執行,最終在靜態方法NewContext被創建。我們調用Invoke方法讓指定的方法對應的應用程序域中執行。
1: public class AppDomainContext
2: {
3: public AppDomain AppDomain { get; private set; }
4: private AppDomainContext(AppDomain appDomain)
5: {
6: this.AppDomain = appDomain;
7: }
8: public static AppDomainContext NewContext(string friendlyName)
9: {
10: return new AppDomainContext(AppDomain.CreateDomain(friendlyName));
11: }
12:
13: public void Invoke<T>(Action<T> action) where T : MarshalByRefObject
14: {
15: T instance = (T)this.AppDomain.CreateInstanceAndUnwrap(typeof(T).Assembly.FullName, typeof(T).FullName);
16: action.Invoke(instance);
17: }
18: }
我們接著在定義一個輔助類ObjectLock方便進行加鎖,以及確認對象是否被所住。ObjectLock比如繼承自,因為我們需要該對象以MBR的方式進行傳遞。在Lock方法中對指定的對象進行加鎖,並指定加鎖的時間。在CheckLock中通過時間間隔判斷指定的對象是否已經被鎖住,相應的結果會在控製台中被輸出。為了讓大家能夠確定相應的操作是在哪個應用程序域中執行的,在枷鎖和檢查鎖定的時候將應用程序域的名稱(AppDomain.FriendlyName屬性)打印出來。
1: public class ObjectLock : MarshalByRefObject
2: {
3: public void Lock(object objectToLock, int millisecondsTimeout)
4: {
5: lock (objectToLock)
6: {
7: Console.WriteLine("[{0}] Successfully lock the object.", AppDomain.CurrentDomain.FriendlyName);
8: Thread.Sleep(millisecondsTimeout);
9: }
10: }
11: public void CheckLock(object objectToLock)
12: {
13: if (Monitor.TryEnter(objectToLock, 10))
14: {
15: Console.WriteLine("[{0}] The object is not locked.", AppDomain.CurrentDomain.FriendlyName);
16: }
17: else
18: {
19: Console.WriteLine("[{0}] The object is locked .", AppDomain.CurrentDomain.FriendlyName);
20: }
21: }
22: }
然後我再一個控製台應用中的Main方法中,編寫了如下簡單的代碼。通過AppDomainContext在一個的應用程序域(Foo)中鎖定一個值為“Hello World!”的字符串,並在另一個應用程序域(Bar)中確認同值得字符串是否已經被鎖定。結果表示在應用程序域Bar中指定的字符串已經被鎖定,從而證明了應用程序域Foo和Bar中兩個值為“Hello World!”的字符串對象實際上是同一個。
1: static void Main(string[] args)
2: {
3: Action<ObjectLock> lockObj = objLock => objLock.Lock("Hello World!", 2000);
4: Action<ObjectLock> checkLock = objLock => objLock.CheckLock("Hello World!");
5:
6: Thread lockObjThread = new Thread(() => AppDomainContext.NewContext("Foo").Invoke<ObjectLock>(lockObj));
7: Thread checkLockThread = new Thread(() => AppDomainContext.NewContext("Bar").Invoke<ObjectLock>(checkLock));
8:
9: lockObjThread.Start();
10: Thread.Sleep(500);
11: checkLockThread.Start();
12: }
輸出結果:
1: 1: [Foo] Successfully lock the object.
2: 2: [Bar] The object is locked.
上麵的介紹同時說明一個問題:。
四、程序集加載的方式
雖然我們說CLR在啟動托管應用的時候,以的方式加載msCorLib.dll這個程序集,但是這不是程序集默認采用的加載方式。在默認的情況下,程序集被加載到當前的程序域中,供該程序集獨占使用。我個人將這兩種不同的程序集加載方式稱為:和。如右圖所示:如果某個類型被定義在程序集中Foo.Dll,當AppDomain1和AppDomain2需要使用該類型的時候,它們會分別以獨占的方式加載程序集Foo.Dll。但是,如果它們使用一些基元類型,比如System.Object、System.Int32、System.DateTime等,則不會加載定義它們的msCorLib.dll程序集,而是直接使用已經被以中立域方式加載到共享程序域中的msCorLib.dll。
我們同樣可以借助上麵定義的AppDomainContext來證明這一點。在這之前我需要說明一點:程序集的加載包括對定義在程序集中類型係統的加載,我們可以通過類型對象的加鎖情況來推斷程序集的加載方式。為此我在上麵創建的解決方案中添加了一個類庫項目Lib,ConsoleApp引用Lib項目,並在Lib中定義了一個空的Foo類型。
1: namespace Artech.MemAllocation
2: {
3: public class Foo
4: {}
5: }
然後我們修改之前的程序,將對字符串加鎖替換在對加鎖。從輸出結果我們可以看出,在Bar程序域中使用的Foo類型並沒有被鎖住,從而證明兩個程序域(Foo和Bar)使用的同一個類型並不是Type對象,因為對應的程序集是以獨占的方式加載的。
1: static void Main(string[] args)
2: {
3: Action<ObjectLock> lockObj = objLock => objLock.Lock(typeof(Foo), 2000);
4: Action<ObjectLock> checkLock = objLock => objLock.CheckLock(typeof(Foo));
5:
6: Thread lockObjThread = new Thread(() => AppDomainContext.NewContext("Foo").Invoke<ObjectLock>(lockObj));
7: Thread checkLockThread = new Thread(() => AppDomainContext.NewContext("Bar").Invoke<ObjectLock>(checkLock));
8:
9: lockObjThread.Start();
10: Thread.Sleep(500);
11: checkLockThread.Start();
12: }
輸出結果:
1: [Foo] Successfully lock the object.
2: [Bar] The object is not locked.
但是,如果我們將加鎖和鎖定檢驗的typeof(Foo)替換成,結果就完全不一樣了。不同的結果說明了msCorLib.dll采用了不同於上麵的程序集加載方式,以中立域方法的加載方式決定在任何應用程序域中使用的類型都是同一個Type對象。
1: static void Main(string[] args)
2: {
3: Action<ObjectLock> lockObj = objLock => objLock.Lock(typeof(int), 2000);
4: Action<ObjectLock> checkLock = objLock => objLock.CheckLock(typeof(int));
5:
6: Thread lockObjThread = new Thread(() => AppDomainContext.NewContext("Foo").Invoke<ObjectLock>(lockObj));
7: Thread checkLockThread = new Thread(() => AppDomainContext.NewContext("Bar").Invoke<ObjectLock>(checkLock));
8:
9: lockObjThread.Start();
10: Thread.Sleep(500);
11: checkLockThread.Start();
12: }
輸出結果:
1: [Foo] Successfully lock the object.
2: [Bar] The object is locked.
五、我們自己的程序集也可以采用中立域的方式加載嗎?
我想到這裏有人會問一個問題:“我們自定義的程序集可以像msCorLib.dll一樣以中立域的方式共享加載嗎?”。對於控製台應用,你隻需要在Main方法上應用LoaderOptimizationAttribute特性,並指定LoaderOptimization為MultiDomain即可。比如,還是采用對Foo類型對象加鎖,這次我們在Main方法上應用了這樣的特性:。輸出的結果就與對Int32類型對象加鎖一樣。
1: [LoaderOptimization(LoaderOptimization.MultiDomain)]
2: static void Main(string[] args)
3: {
4: Action<ObjectLock> lockObj = objLock => objLock.Lock(typeof(Foo), 2000);
5: Action<ObjectLock> checkLock = objLock => objLock.CheckLock(typeof(Foo));
6:
7: Thread lockObjThread = new Thread(() => AppDomainContext.NewContext("Foo").Invoke<ObjectLock>(lockObj));
8: Thread checkLockThread = new Thread(() => AppDomainContext.NewContext("Bar").Invoke<ObjectLock>(checkLock));
9:
10: lockObjThread.Start();
11: Thread.Sleep(500);
12: checkLockThread.Start();
13: }
輸出結果:
1: [Foo] Successfully lock the object.
2: [Bar] The object is locked.
又一個關於加鎖的注意:。
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-27 14:04:42