關於CLR內存管理一些深層次的討論[下篇]
《上篇》中我們主要討論的是和的話題,著重介紹了兩個不同的程序集加載方式——方式和方式(中立域方式);以及基於的字符串駐留。這篇將關注點放在托管對象創建時和對上,不對之處,還望各位能夠及時指出。
目錄
一、從類型(Type)與實例(Instance)談起
二、實例內存分配不僅限於GC堆
三、實例對類型的引用
四、LOH中的對象如何被回收
一、從類型(Type)與實例(Instance)談起
在麵向對象的世界中,類型和實例是兩個核心的要素。不論是類型和實例,相關的信息比如加載到內存中,對應著某一塊或者多塊連續或者不連續的內存。那麼對類型和實例的內存分配時如何進行的呢?對象是“狀態”和“行為”的組合體,所以從.NET Framework的角度來看類型,它隻具有兩種類型的成員——和(實際還有嵌套類型),前者表示狀態,後者表示行為。類型是對元數據的描述,而實例則是符合該元數據描述的單個個體。同一個類型下的所有實例具有相同的行為,它們通過狀態值的不同得以區分。所以內存中的實例(本篇所說的實例指代的實例)表示的是字段值,而內存中的類型表示的則是類型成員結構的元數據。很多人都知道,當我們創建一個對象的時候,CLR會在(Heap)中開辟一塊連續的內存空間保存字段值。那麼類型信息又是保存在那塊內存上呢?
實際上,類型信息保存在“另一堆”上,我們稱之為。每一個應用程序域都具有各自的加載器堆,即包括我們創建的普通應用程序域,也包括《上篇》中提到的三個特殊應用程序域:係統程序域、共享程序域和默認程序域。如果說GC堆是實例的容器,那麼基於應用程序域的。CLR采用“”的運行機製。當某個類型被第一次使用的時候,CLR試圖加載該類型。如果該類型對應的程序沒有獨自地加載到本應用程序域中,或者沒有通過中立域的形式加載到共享程序域中,它會按照相應的方式加載程序集(在這裏我們假設采用加載)。然後,將使用到的這個類型加載到本應用程序域的加載器堆中。
加載器堆維護著自應用程序域創建以來使用過的所有類型記錄,它們對應著一個特殊的對象——。當程序第一次執行到某個方法的時候,CLR會定位到方法表中該條目,獲取相關信息進行JIT編譯。所以如果某個類型在加載器堆中的方法表的某個條目至少被執行一次,它就會指向一段JIT編譯後的機器指令。
二、實例內存分配不僅限於GC堆
到現在為止,我們知道了類型和實例分別分配於基於應用程序域的加載器堆和GC堆中,那麼CLR的內存分配僅僅限於這“兩堆”嗎?當然不是,除了這“兩堆”以及默認的進程堆,還有額外“兩堆”,一是存放JIT編譯後機器指令的,另一個則是專門用於“大對象”的。下圖反映了CLR主要維護的這些個不同的“堆”。
對於大對象堆,在本文後續部分還會講述,在這裏我們需要先了解CLR認為怎樣的對象是“大對象”。當我們實例化一個對象的時候,如果該對象大於或者等於(這種對象一般是數組,一般對象不會這麼大),CLR將認為是“大對象”並被放到LOH中,否則放到GC堆中。這裏有一點需要讀者注意的是,作為垃圾回收器的GC並不僅僅限於針對GC堆中對象的回收,LOH中的對象的回收工作通過在GC的管轄之下。所以從某種意義上講:你可以將之前提到的GC堆理解為SOH(Small
Object Heap),或者稱之為“狹義GC堆”,而將“廣義GC堆”理解為。
三、實例對類型的引用
實例是類型的實例,實例和它所對應的類型需要維持一種聯係。反映在內存中,就以為著。實例對類型的引用通過一個特殊的對象來維係——T。我們舉個例子,在如下一段簡單的對象實例化代碼中 ,我先後實例化了四個對象:、、和。
1: string strInstance = "ABC";
2: object objectInstance = new object();
3: Bar barInstance = new Bar()
4: byte[] largeObjInstance = new byte[85000];
當上麵的程序執行後,圍繞著實例化的四個對象和類型信息,在內存中將會具有如下一個關係。最左邊的是現成調用棧中的上述四個變量,對於字符串類型的strInstance,由於《上篇》所講述的關於機製,最後總的字符串被分配到中;Object和Bar類型的objectInstance與barInstance由於是小於85000字節的小對象,所以被分配到中。objectInstance通過TypeHandle指向位於中System.Objhect類型對應的方法表(因為定義該類型的mscorlib程序集以中立域的方式加載),而barInstance得TypeHandle指向的基於Bar類型的方法表則位於中(因為程序域默認采用獨占的方式加載)。元素個數為85000的字節數組largeObjInstance屬於大對象,直接分配到中。largeObjInstance的TypeHandle指向的基於System.Byte[]類型的方法表,該System.Byte[]類型同樣定義在mscorlib程序集中,所以該方法表同樣存在於共享程序域的加載器堆。
四、LOH中的對象如何被回收
了解GC的讀者應該都知道CLR采用基於“”的垃圾回收機製。代齡,個人覺得是一個很準確的詞語,它充分體現了設計者用於表現“”的意思。所有對象分三代,即G0、G1和G2,這實際上代表了三個不同的連續的內存塊。“輩分”越高,表明時間越久;“輩分”越低,被掃蕩(GC回收)的頻率就越高。關於基於代齡的垃圾回收機製,限於篇幅,就說到這裏。我們的重點是GC采用怎樣的機製對LOH的對象進行回收。
到目前為止,對於LOH和GC堆中的對象,除了大小之外,我們好像沒有覺得它們之間有何不同。實際上,將大對象放在LOH中,目的在於對其實施特殊的回收機製。關於垃圾收回,我們應該有這樣的認知:回收的成本是和對象的大小基本成“正向”關係,。所以我們不能對大對象頻繁地實施垃圾回收,實際上CLR是將LOH對象當成的對象。也就是說,針對LOH的回收工作是和GC堆中一並進行的。換句話說,。關於LOH的垃圾回收機製,我們可以通過一個非常簡單的程序來驗證。
1: class Program
2: {
3: static WeakReference SmallObjRef;
4: static WeakReference LargeObjRef;
5:
6: static void Main(string[] args)
7: {
8: SetValues();
9: GC.Collect(0);
10: Console.WriteLine("GC.Collect(0)");
11: Console.WriteLine("SmallObjRef.Target == null? {0}", SmallObjRef.Target == null);
12: Console.WriteLine("LargeObjRef.Target == null? {0}\n", LargeObjRef.Target == null);
13:
14: GC.Collect(1);
15: Console.WriteLine("GC.Collect(1)");
16: Console.WriteLine("LargeObjRef.Target == null? {0}\n", LargeObjRef.Target == null);
17:
18: GC.Collect(2);
19: Console.WriteLine("GC.Collect(2)");
20: Console.WriteLine("LargeObjRef.Target == null? {0}\n", LargeObjRef.Target == null);
21: }
22:
23: static void SetValues()
24: {
25: SmallObjRef = new WeakReference(new byte[84000]);
26: LargeObjRef = new WeakReference(new byte[85000]);
27: }
28: }
輸出結果:
1: GC.Collect(0)
2: SmallObjRef.Target == null? True
3: LargeObjRef.Target == null? False
4:
5: GC.Collect(1)
6: LargeObjRef.Target == null? False
7:
8: GC.Collect(2)
9: LargeObjRef.Target == null? True
在上麵的代碼中沒,我創建了兩個WeakReference對象,它們的Target分別被設置成byte[84000]和byte[85000]。按照我們上麵關於對“大對象”的界定,後者是大對象,前者不是。然後,我們先後三次對G0、G1和G2實施垃圾回收,我們發現“小對象”在實施針對G0的垃圾回收後就沒了;而“大對象”會一直存活直到針對G2的垃圾回收被執行。
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-27 14:04:40