1. 程式人生 > >【譯】.Net 垃圾回收機制原理(轉)

【譯】.Net 垃圾回收機制原理(轉)

時有 沒有 finalize 遇到 keyword ren 以及 保留字 rac

上一篇文章介紹了.Net 垃圾回收的基本原理和垃圾回收執行Finalize方法的內部機制;這一篇我們看下弱引用對象,代,多線程垃圾回收,大對象處理以及和垃圾回收相關的性能計數器。

讓我們從弱引用對象說起,弱引用對象可以減輕大對象帶來的內存壓力。

弱引用(Weak References)

當程序的根對象指向一個對象時,這個對象是可達的,垃圾回收器不能回收它,這稱為對對象的強引用。和強引用相對的是弱引用,當一個對象上存在弱引用時,垃圾回收器可以回收此對象,但是也允許程序訪問這個對象。這是怎麽回事兒呢?請往下看。

如果一個對象上僅存在弱引用,並且垃圾回收器在運行,這個對象就會被回收,之後如果程序中要訪問這個對象,訪問就會失敗。另一方面,要使用弱引用的對象,程序必須先對這個對象進行強引用,如果程序在垃圾回收器回收這個對象之前對對象進行了強引用,這樣(有了強引用之後)垃圾回收器就不能回收此對象了。這有點繞,讓我們用一段代碼來說明一下:

void Method() { //創建對象的強引用 Object o = new Object(); // 用一個短弱引用對象弱引用o. WeakReference wr = new WeakReference(o); o = null; // 移除對象的強引用 o = wr.Target; //嘗試從弱引用對象中獲得對象的強引用 if (o == null) { // 如果對象為空說明對象已經被垃圾回收器回收掉了 } else { // 如果垃圾回收器還沒有回收此對象就可以繼續使用對象了 } }

為什麽需要弱對象呢?因為,有一些數據創建起來很容易,但是卻需要很多內存。例如:你有一個程序,這個程序需要訪問用戶硬盤上的所有文件夾和文件名;你可以在程序第一次需要這個數據時訪問用戶磁盤生成一次數據,數據生成之後你就可以訪問內存中的數據來得到用戶文件數據,而不是每次都去讀磁盤獲得數據,這樣做可以提升程序的性能。

問題是這個數據可能相當大,需要相當大的內存。如果用戶去操作程序的另外一部分功能了,這塊相當大的內存就沒有占用的必要了。你可以通過代碼刪除這些數據,但是如果用戶馬上切換到需要這塊數據的功能上,你就必須重新從用戶的磁盤上構建這個數據。弱引用為這種場景提供了一種簡單有效的方案。

當用戶切換到其他功能時,你可以為這個數據創建一個弱引用對象,並把對這個數據的強引用解除掉。這樣如果程序占用的內存很低,垃圾回收操作就不會觸發,弱引用對象就不會被回收掉;這樣當程序需要使用這塊數據時就可以通過一個強引用來獲得數據,如果成功得到了對象引用,程序就沒有必要再次讀取用戶的磁盤了。

WeakReference類型提供了兩個構造函數:

WeakReference(object target); WeakReference(object target, bool trackResurrection);

target參數顯然就是弱引用要跟蹤的對象了。trackResurrection參數表示當對象的Finalize方法執行之後是否還要跟蹤這個對象。默認這個參數是false。有關對象的復活請參考這裏。

方便起見,不跟蹤復活對象的弱引用稱為“短弱引用”;而要跟蹤復活對象的的弱引用稱為“長弱引用”。如果對象沒有實現Finalize方法,那麽長弱引用和短弱引用是完全一樣的。強烈建議你盡量避免使用長弱引用。長弱引用允許你使用復活的對象,而復活對象的行為可能是不可以預知的。

一旦你使用WeakReference引用了一個對象,建議你將這個對象的所有強用都設置為null;如果強引用存在的話,垃圾回收器是永遠都不可能回收弱引用指向的對象的。

當你要使用弱引用目標對象時,你必須為目標對象創建一個強引用,這很簡單,只要用object a = weekRefer.Target;就可以了,然後你必須判斷a是否為空,弱不為空才可以繼續使用,弱為空就表示對象已經被垃圾回收器回收了,得通過其他方法重新獲得此對象。

弱引用的內部實現

從前文中的描述中我們可以推斷出弱引用對象肯定和一般對象的處理是不一樣的。一般情況下如果一個對象引用了另一個對象就是強引用,垃圾回收器就不能回收被引用的對象,而WeakReference對象卻不是這樣子,它引用的對象是有可能被回收的。

要完全理解弱對象是如何工作的,我們還需要看一下托管堆。托管堆上有兩個內部數據結構他們的唯一作用是管理弱引用:我們可以把它們稱作長弱引用表和短弱引用表;這兩個表存放托管堆上的弱引用目標對象指針。

程序運行之初,這兩個表都是空的。當你創建一個WeakReference對象時,這個對象並不是分配到托管堆上的,而是在弱對象表中創建一個空槽(Empty Slot)。短弱引用對象被放在短弱對象表中,長弱引用對象被放在長弱引用表中。

一旦發現空槽,空槽的值會被設置成弱引用目標對象的地址;顯然長短弱對象表中的對象是不會當作應用程序的根對象的。垃圾回收器不會回收長短弱對象表中的數據。

讓我們來看下垃圾回收執行時發生了什麽:
1. 垃圾回收器構建一個可達對象圖,構建步驟請參考上文
2. 垃圾回收器掃描短弱對象表,如果弱對象表中指向的對像沒有在可達對象圖中,那麽這個對像就被標識為垃圾對象,然後短對象表中的對象指針被設置為空
3. 垃圾回收器掃描終結隊列(參考上文),如果隊列中的對象不在可達對象圖中,這個對象從終結隊列中移動到Freachable隊列中,這時候,這個對象又被標識為可達對象,不再是垃圾了
4. 垃圾回收器掃描長弱引用表。如果表中的對象不在可達對象圖中(可達對象圖中包括在Freachable隊列中對象),將長引用對象表中對應的對象指針設置為null
5. 垃圾回收器移動可達對象

一旦你理解了垃圾回收器的工作過程,就很容易理解弱引用是如何起作用了。訪問WeakReference的Target屬性導致系統返回弱對象表中的目標對象指針,如果是null,表示對象已經被回收了。

短弱引用不跟蹤復活,這意味著垃圾回收器可以在掃描終結隊列之前檢查弱引用表中指向的對象是否是垃圾對象。

而長弱引用跟蹤復活對象,這意味著垃圾回收器必須在確認對象回收之後才可以將弱引用表中的指針設置為null。

代:

提起.Net的垃圾回收,c++或者c程序員可能就會想,這麽管理內存會不會出現性能問題呢。GC的開發人員一直在調整垃圾回收器提升它的性能。代就是一種為了降低垃圾回收對性能影響的機制。垃圾回收器在工作時會假定如下說法是成立的:

1. 一個對象越新,那麽這個對象的生命周期就越短
2. 一個對象越老,那麽這個對象的生命周期就越長
3. 新對象之間通常更可能和新對象之間存在引用關系
4. 壓縮堆的一部分要比壓縮整個堆要快

當然大量研究證明以上幾個假設在很多程序上是成立的。那就讓我們來談談這幾個假設是如何影響垃圾回收器工作的吧。

在程序初始化時,托管堆上沒有對象。這時候新添到托管堆上的對象是的代是0.如下圖所示,0代對象是最年輕的對象,他們從來沒有經過垃圾回收器的檢查。

技術分享圖片

圖1 托管堆上的0代對象

現在如果堆上添加了更多的對象,堆填滿時就會觸發垃圾回收。當垃圾回收器分析托管堆時,會構建一個垃圾對象(圖2中淺紫色塊)和非垃圾對象的圖。所有沒有被回收的對象會被移動壓縮到堆的最底端。這些沒有被回收掉的對象就成為了1代對象,如圖2所示

技術分享圖片

圖2 托管堆上的0代1代對象

當堆上分配了更多的對象時,新對象被放在了0代區。如果0代堆填滿了,就會觸發一次垃圾回收。這時候活下來的對象成為1代對象被移動到堆的底部;再此發生垃圾回收後1代對象中存活下來的對象會提升為2代對象並被移動壓縮。如圖3所示:

技術分享圖片

圖3 托管堆上的0、1、2代對象
2代對象是目前垃圾回收器的最高代,當再次垃圾回收時,沒有回收的對象的代數依然保持2.

垃圾回收分代為什麽可以優化性能

如前所述,分代回收可以提高性能。當堆填滿之後會觸發垃圾回收,垃圾回收器可以只選擇0代上的對象進行回收,而忽略更高代堆上的對象。然而,由於越年輕的對象生命周期越短,因此,回收0代堆可以回收相當多的內存,而且回收所耗的性能也比回收所有代對象要少得多。

這是分代垃圾回收的最簡單優化。分代回收不需要便利整個托管堆,如果一個根對象引用了一個高代對象,那麽垃圾回收器可以忽略高代對象和其引用對象的遍歷,這會大大減少構建可達對象圖的時間。

如果回收0代對象沒有釋放出足夠的內存,垃圾回收器會嘗試回收1代和0代堆;如果仍然沒有獲得足夠的內存,那麽垃圾回收器會嘗試回收2,1,0代堆。具體會回收那一代對象的算法不是確定的,微軟會持續做算法優化。

多數堆(像c-runtime堆)只要找到足夠的空閑內存就分配給對象。因此,如果我連續分配多個對象時,這些對象的地址空間可能會相差幾M。然而在托管堆上,連續分配的對象的內存地址是連續的。

前面的假設中還提到,新對象之間更可能存在相互引用關系。因此新對象分配到連續的內存上,你可以獲得就近引用的性能優化(you gain performance from locality of reference)。這樣的話很可能你的對象都在CPU的緩存中,這樣CPU的很多操作就不需要去存取內存了。

微軟的性能測試顯示托管堆的分配速度比標準的win32 HeapAlloc方法還要快。這些測試也顯示了200MHz的Pentium的CPU做一次0代回收時間可以小於1毫秒。微軟的優化目的是讓垃圾回收耗用的時間小於一次普通的頁面錯誤。

使用System.GC類控制垃圾回收

類型System.GC運行開發人員直接控制垃圾回收器。你可以通過GC.MaxGeneration屬性獲得GC的最高代數,目前最高代是定值2.

你可以調用GC.Collect()方法強制垃圾回收器做垃圾回收,Collect方法有兩個重載:

void GC.Collect(Int32 generation) void GC.Collect()

第一個方法允許你指定要回收那一代。你可以傳0到GC.MaxGeneration的數字做參數,傳0只做0代堆的回收,傳1會回收1代和0代堆,而傳2會回收整個托管堆。而無參數的方法調用GC.Collect(GC.MaxGeneration)相當於整個回收。

在通常情況下,不應該去調用GC.Collect方法;最好讓垃圾回收器按照自己的算法判斷什麽時候該調用Collect方法。盡管如此,如果你確信比運行時更了解什麽時候該做垃圾回收,你就可以調用Collect方法去做回收。比如說程序可以在保存數據文件之後做一次垃圾回收。比如你的程序剛剛用完一個長度為10000的大數組,你不再需要他了,就可以把它設置為null然後執行垃圾回收,緩解內存的壓力。

GC還提供了WaitForPendingFinalizers方法。這個方法簡單的掛起執行線程,知道Freachable隊列中的清空之後,執行完所有隊列中的Finalize方法之後才繼續執行。

GC還提供了兩個方法用來返回某個對象是幾代對象,他們是

Int32 GC.GetGeneration(object o); Int32 GC.GetGeneration(WeakReference wr)

第一個方法返回普通對象是幾代,第二個方法返回弱引用對象的代數。

下面的代碼可以幫助你理解代的意義:

private static void GenerationDemo() { // Let‘s see how many generations the GCH supports (we know it‘s 2) Display("Maximum GC generations: " + GC.MaxGeneration); // Create a new BaseObj in the heap GenObj obj = new GenObj("Generation"); // Since this object is newly created, it should be in generation 0 obj.DisplayGeneration(); // Displays 0 // Performing a garbage collection promotes the object‘s generation GC.Collect(); obj.DisplayGeneration(); // Displays 1 GC.Collect(); obj.DisplayGeneration(); // Displays 2 GC.Collect(); obj.DisplayGeneration(); // Displays 2 (max generation) obj = null; // Destroy the strong reference to this object GC.Collect(0); // Collect objects in generation 0 GC.WaitForPendingFinalizers(); // We should see nothing GC.Collect(1); // Collect objects in generation 1 GC.WaitForPendingFinalizers(); // We should see nothing GC.Collect(2); // Same as Collect() GC.WaitForPendingFinalizers(); // Now, we should see the Finalize // method run Display(-1, "Demo stop: Understanding Generations.", 0); } class GenObj{ public void DisplayGeneration(){ Console.WriteLine(“my generation is ” + GC.GetGeneration(this)); } ~GenObj(){ Console.WriteLine(“My Finalize method called”); } }

垃圾回收機制的多線程性能優化

在前面的部分,我解釋了GC的算法和優化,然後討論的前提都是在單線程情況下的。而在真實的程序中,很可能是多個線程一起工作,多個線程一起操縱托管堆上的對象。當一個線程觸發了垃圾回收,其他所有的線程都應該暫停訪問任何引用對象(包括他們自己棧上引用的對象),因為垃圾回收器有可能要移動對象,修改對象的內存地址。

因此當垃圾回收器開始回收時,所有執行托管代碼的線程必須掛起。運行時有幾種不同的機制可以安全的掛起線程來執行垃圾回收。這一塊的內部機制我不打算詳細說明。但是微軟會持續修改垃圾回收的機制來降低垃圾回收帶來的性能損耗。

下面幾段描述了垃圾回收器在多線程情況下是如何工作的:
完全中斷代碼執行 當垃圾回收開始執行時,掛起所有應用程序線程。垃圾回收器隨後將線程掛起的位置記錄到一個just-in-time(JIT)編譯器生成的表中,垃圾回收器負責將線程掛起的位置記錄在表中,記錄當前正在訪問的對象,以及對象存放的位置(變量中,CPU寄存器中,等等)
劫持:垃圾回收器可以修改線程的棧讓返回地址指向一個特殊的方法,當當前執行的方法返回時,這個特殊的方法將會執行,掛起線程,這種改變線程執行路徑的方式稱為劫持線程。當垃圾回收完成之後,線程會重新返回到之前執行的方法上。

安全點: 當JIT編譯器編譯一個方法時,可以在某個點插入一段代碼判斷GC是否掛起,如果是,線程就掛起等待垃圾回收完成,然後線程重新開始執行。JIT編譯器插入檢查GC代碼的位置被稱作“安全點”

請註意,線程劫持允許正在執行非托管代碼的線程在垃圾回收過程中執行。如果非托管代碼不訪問托管堆上的對象時這是沒有問題的。如果這個線程當前執行非托管代碼然後返回執行托管代碼,這個線程將會被劫持,直到垃圾回收完成之後再繼續執行。

除了我剛提到的集中機制之外,垃圾回收器還有其他改進來增強多線程程序中的對象內存分配和回收。

同步釋放分配(Synchronization-free Allocations):在一個多線程系統中,0代堆被分成幾個區域,一個線程使用一個區域。這允許多線程同時分配對象,並不需要一個線程獨占堆。

可伸縮回收(Scalable Collections):在多線程系統中運行執行引擎的服務器版本(MXSorSvr.dll).托管堆會被分成幾個不同的區域,一個CPU一個區域。當回收初始化時,每個CPU執行一個回收線程,各個線程回收各自的區域。而工作站版本的執行引擎(MXCorWks.dll)不支持這個功能。

大對象回收

這一塊就不翻譯了,有一篇專門的文章談這件事兒

監視垃圾回收

如果你安裝了.Net framework你的性能計數器(開始菜單—管理工具—性能 進入)中就會有.Net CLR Memory一項,你可以從實例列表中選擇某個程序進行觀察,如下圖所示。

技術分享圖片

這些性能指標的具體含義如下:

性能計數器

說明

# Bytes in all Heaps(所有堆中的字節數)

顯示以下計數器值的總和:“第 0 級堆大小”計數器、“第 1 級堆大小”計數器、“第 2 級堆大小”計數器和“大對象堆大小”計數器。此計數器指示在垃圾回收堆上分配的當前內存(以字節為單位)。

# GC HandlesGC 處理數目)

顯示正在使用的垃圾回收處理的當前數目。垃圾回收處理是對公共語言運行庫和托管環境外部的資源的處理。

# Gen 0 Collections(第 2 級回收次數)

顯示自應用程序啟動後第 0 級對象(即最年輕、最近分配的對象)被垃圾回收的次數。

當第 0 級中的可用內存不足以滿足分配請求時發生第 0 級垃圾回收。此計數器在第 0 級垃圾回收結束時遞增。較高級的垃圾回收包括所有較低級的垃圾回收。當較高級(第 1 級或第 2 級)垃圾回收發生時此計數器被顯式遞增。

此計數器顯示最近的觀察所得值。_Global_ 計數器值不準確,應該忽略。

# Gen 1 Collections(第 2 級回收次數)

顯示自應用程序啟動後對第 1 級對象進行垃圾回收的次數。

此計數器在第 1 級垃圾回收結束時遞增。較高級的垃圾回收包括所有較低級的垃圾回收。當較高級(第 2 級)垃圾回收發生時此計數器被顯式遞增。

此計數器顯示最近的觀察所得值。_Global_ 計數器值不準確,應該忽略。

# Gen 2 Collections(第 2 級回收次數)

顯示自應用程序啟動後對第 2 級對象進行垃圾回收的次數。此計數器在第 2 級垃圾回收(也稱作完整垃圾回收)結束時遞增。

此計數器顯示最近的觀察所得值。_Global_ 計數器值不準確,應該忽略。

# Induced GC(引發的 GC 的數目)

顯示由於對 GC.Collect 的顯式調用而執行的垃圾回收的峰值次數。讓垃圾回收器對其回收的頻率進行微調是切實可行的。

# of Pinned Objects(釘住的對象的數目)

顯示上次垃圾回收中遇到的釘住的對象的數目。釘住的對象是垃圾回收器不能移入內存的對象。此計數器只跟蹤被進行垃圾回收的堆中的釘住的對象。例如,第 0 級垃圾回收導致僅枚舉第 0 級堆中釘住的對象。

# of Sink Blocks in use(正在使用的接收塊的數目)

顯示正在使用的同步塊的當前數目。同步塊是為存儲同步信息分配的基於對象的數據結構。同步塊保留對托管對象的弱引用並且必須由垃圾回收器掃描。同步塊不局限於只存儲同步信息;它們還可以存儲 COM interop 元數據。該計數器指示與同步基元的過度使用有關的性能問題。

# Total committed Bytes(提交字節的總數)

顯示垃圾回收器當前提交的虛擬內存量(以字節為單位)。提交的內存是在磁盤頁面文件中保留的空間的物理內存。

# Total reserved Bytes(保留字節的總數)

顯示垃圾回收器當前保留的虛擬內存量(以字節為單位)。保留內存是為應用程序保留(但尚未使用任何磁盤或主內存頁)的虛擬內存空間。

% Time in GCGC 中時間的百分比)

顯示自上次垃圾回收周期後執行垃圾回收所用運行時間的百分比。此計數器通常指示垃圾回收器代表該應用程序為收集和壓縮內存而執行的工作。只在每次垃圾回收結束時更新此計數器。此計數器不是一個平均值;它的值反映了最近觀察所得值。

Allocated Bytes/second(每秒分配的字節數)

顯示每秒在垃圾回收堆上分配的字節數。此計數器在每次垃圾回收結束時(而不是在每次分配時)進行更新。此計數器不是一段時間內的平均值;它顯示最近兩個樣本中觀測的值的差除以取樣間隔時間所得的結果。

Finalization Survivors(完成時存留對象數目)

顯示因正等待完成而從回收後保留下來的進行垃圾回收的對象的數目。如果這些對象保留對其他對象的引用,則那些對象也保留下來,但此計數器不對它們計數。“從第 0 級提升的完成內存”和“從第 1 級提升的完成內存”計數器表示因完成而保留下來的所有內存。

此計數器不是累積計數器;它在每次垃圾回收結束時由僅在該特定回收期間存留對象的計數更新。此計數器指示由於完成應用程序可能導致系統開銷過高。

Gen 0 heap size(第 2 級堆大小)

顯示在第 0 級中可以分配的最大字節數;它不指示在第 0 級中當前分配的字節數。

當自最近回收後的分配超出此大小時發生第 0 級垃圾回收。第 0 級大小由垃圾回收器進行微調並且可在應用程序執行期間更改。在第 0 級回收結束時,第 0 級堆的大小是 0 字節。此計數器顯示調用下一個第 0 級垃圾回收的分配的大小(以字節為單位)。

此計數器在垃圾回收結束時(而不是在每次分配時)進行更新。

Gen 0 Promoted Bytes/Sec(從第 1 級提升的字節數/秒)

顯示每秒從第 0 級提升到第 1 級的字節數。內存在從垃圾回收保留下來後被提升。此計數器是每秒創建的在相當長時間保留下來的對象的指示符。

此計數器顯示在最後兩個樣本(以取樣間隔持續時間來劃分)中觀察到的值之間的差異。

Gen 1 heap size(第 2 級堆大小)

顯示第 1 級中的當前字節數;此計數器不顯示第 1 級的最大大小。不直接在此代中分配對象;這些對象是從前面的第 0 級垃圾回收提升的。此計數器在垃圾回收結束時(而不是在每次分配時)進行更新。

Gen 1 Promoted Bytes/Sec(從第 1 級提升的字節數/秒)

顯示每秒從第 1 級提升到第 2 級的字節數。在此計數器中不包括只因正等待完成而被提升的對象。

內存在從垃圾回收保留下來後被提升。不會從第 2 級進行任何提升,因為它是最舊的一級。此計數器是每秒創建的非常長時間保留下來的對象的指示符。

此計數器顯示在最後兩個樣本(以取樣間隔持續時間來劃分)中觀察到的值之間的差異。

Gen 2 heap size(第 2 級堆大小)

顯示第 2 級中當前字節數。不直接在此代中分配對象;這些對象是在以前的第 1 級垃圾回收期間從第 1 級提升的。此計數器在垃圾回收結束時(而不是在每次分配時)進行更新。

Large Object Heap size(大對象堆大小)

顯示大對象堆的當前大小(以字節為單位)。垃圾回收器將大於 20 KB 的對象視作大對象並且直接在特殊堆中分配大對象;它們不是通過這些級別提升的。此計數器在垃圾回收結束時(而不是在每次分配時)進行更新。

Promoted Finalization-Memory from Gen 0(從第 1 級提升的完成內存)

顯示只因等待完成而從第 0 級提升到第 1 級的內存的字節數。此計數器不是累積計數器;它顯示在最後一次垃圾回收結束時觀察到的值。

Promoted Finalization-Memory from Gen 1(從第 1 級提升的完成內存)

顯示只因等待完成而從第 1 級提升到第 2 級的內存的字節數。此計數器不是累積計數器;它顯示在最後一次垃圾回收結束時觀察到的值。如果最後一次垃圾回收就是第 0 級回收,此計數器則重置為 0。

Promoted Memory from Gen 0(從第 1 級提升的內存)

顯示在垃圾回收後保留下來並且從第 0 級提升到第 1 級的內存的字節數。此計數器中不包括那些只因等待完成而提升的對象。此計數器不是累積計數器;它顯示在最後一次垃圾回收結束時觀察到的值。

Promoted Memory from Gen 1(從第 1 級提升的內存)

顯示在垃圾回收後保留下來並且從第 1 級提升到第 2 級的內存的字節數。此計數器中不包括那些只因等待完成而提升的對象。此計數器不是累積計數器;它顯示在最後一次垃圾回收結束時觀察到的值。如果最後一次垃圾回收就是第 0 級回收,此計數器則重置為 0。

這個表來自MSDN

全文完。原文地址:http://msdn.microsoft.com/zh-cn/magazine/bb985011(en-us).aspx

原作者是:Jeffrey Richter,感謝原作者

【譯】.Net 垃圾回收機制原理(轉)