CLR via C#垃圾回收

垃圾回收平臺的工作原理

對資源的訪問

  1. 調用IL指令newobj,爲資源的類型分配內存
  2. 初始化內存,設置資源的初始狀態,這一步由構造方法執行
  3. 訪問類型的成員
  4. 訪問結束後摧毀資源狀態進行清理
  5. 由垃圾回收器處理,釋放內存

使用IL指令newobj創建對象時,CLR會執行下面步驟:
1. 計算類型的字段所需要的字節數
2. 加上對象的開銷(對象指針和同步快索引)所需的字節數,對於32位程序需要加8字節,64位需要加16字節
3. CLR檢查保留區能否夠提供分配對象所需的字節數,如果夠用對象會被放入,如果不夠會進行調撥。

垃圾回收器的追蹤算法:

根的概念

  1. 每個應用程序都包含一組根(root),每個根都是一個存儲位置,它包含指向引用類型對象的指針。(根只能是引用類型)
  2. 靜態字段被認爲是一個根。
  3. 任何方法參數或局部變量也被認爲是跟。
  4. cpu寄存器

首先,起始時垃圾回收器(GC)會認爲所有對象都是垃圾。

第一階段:標記(marking)

在此階段,GC沿着線程棧上行來檢查所有的根,如果發現該根引用了一個堆中對象,那麼就在對象的“同步塊索引字段”上設置一個bit,即進行“標記”操作。

下圖展示了一個堆,應用程序直接引用對象A,C,D,F,這些對象被標記,然後在標記D時,垃圾回收器發現該對象有個引用了對象H的字段,因此H對象也被標記。垃圾回收器就是以這種遞歸的方式遍歷所有可達的對象。
標記

第二階段:壓縮(compact)

在這個階段,GC線性遍歷堆,尋找未被標記的“垃圾”對象,如果垃圾對象內存塊較小,GC會忽略它;但是如果發現大的、可用的連續內存塊,GC會把非垃圾對象移動到這裏,用來壓縮堆

壓縮

終結操作

Finalize方法在垃圾回收結束時被調用,一下5種事件會導致開始垃圾回收:
1. 第0代滿,這是目前導致Finalize方法被調用的最常見的一種方式.
2. 代碼顯示調用System.GC的靜態方法Collect.
3. Windows報告內存不足,這是CLR會進行強制垃圾回收.
4. CLR卸載APPDomain,一個AppDomain被卸載時,CLR認爲該AppDomain中不存在任何根,因此會對所有代執行垃圾回收.
5. CLR關閉,會調用託管堆中所以的Finalize方法

C#的using語句

在using語句中,我們初始化一個對象,並將它的引用保存到一個變量中,然後在using語句的大括號中訪問該變量。編譯這段代碼時,編譯器自動生成一個try finally塊,在finally塊中編譯器會將變量轉型成IDisposal,並調用Dispose方法。因此using語句只能用於實現了IDisposable接口的類型。

基於代的垃圾回收

代是CLR垃圾回收器採用的一種機制,目的是提高應用程序的性能。一個基於代的垃圾回收器做了一下三點假設:

  1. 對象越新,生存期越短
  2. 對象越老,生存期越長
  3. 一次回收部分堆比回收整個堆更快

回收過程

堆在初始化時沒有對象,最初添加到堆中的對象稱爲第0代對象。CLR初始化時會爲0代空間分配一個預算的大小,假定爲264KB。如果分配一個新對象時超過了這個預算大小就會啓動一次垃圾回收。

下圖所示ABCDE被分配到0代空間,程序運行一段時間後,C,E不可達
001

當要分配對象F時,由於0代已經沒有空間可供分配,這時需要執行垃圾回收,回收CE,與此同時執行壓縮,將ABD存放到連續空間。最後存活的對象ABD被提升到1代空間(1代空間分配的大小要大於0代空間)。0代空間爲空。

執行垃圾收集後的堆空間如下所示:
002

每次垃圾回收之後,新的對象總是會被分配到0代空間中。
接下來當我們在分配F到K的對象時,堆的空間如下所示:
003

現在假定分配新的對象L會造成0代空間滿,需要執行GC 。GC在執行垃圾回收前會檢查1代已使用的空間大小是否超過配額,沒有的話就不對1代執行檢查。
所以GC只檢查0代空間的對象,發現H和J不可用,可以回收。

經過壓縮後,堆空間中的分配如下圖所示:
004

上圖中1代空間中的B沒有被回收,垃圾收集器只回收了0代空間中的不可用的對象。

接下來我們在分配L到0的對象到0代內存空間中,分配後堆空間如下圖所示:
005

當分配對象P時,假設0代空間已滿,執行GC後0代提升爲1代,但由於第1代仍未超過配額,所以不對1代處理。

執行GC後堆空間如下所示
006

可以看到1代的被分配的空間在不斷增大,可用配額逐漸減少。我們再次分配對象P到S。

分配後堆空間圖如下所示:
007

當分配對象T時,假設0代空間已滿,執行GC後0代要提爲1代,假設此時1代對象分配所佔空間已達到最大配額,這時GC會檢查1代堆空間中的對象,回收不可用對象,並執行壓縮,然後1代提爲2代。

回收後的堆空間如下圖所示:
008

CLR的託管堆只支持3代:第0代、第1代、第2代。CLR初始化時,會爲每一代做預算,0代約爲256KB,1代約爲2MB,2代約爲10MB。

如果垃圾回收器發現在回收0代後存活下來的對象很少,那就有可能降低0代的預算。

垃圾收集器會多次回收低代堆空間中的對象後才執行高代內存空間中的對象回收,這樣做大大提高了程序的性能。

參考資料:

CLR via C# (Jeffrey Tichter)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章