淺析CLR的GC(垃圾回收器)

 

文章目錄:

  1. 瞭解託管堆和GC
  2. GC高效的處理方式—代
  3. 特殊類型的清理
  4. 手動監控和控制對象生命週期

一、瞭解託管堆和GC

  在面向對象環境中,每個類型都表明了一種資源。咱們要使用這些資源,就要爲這些表明資源的類型分配內存。在C#中,咱們通常使用new關鍵字來完成。訪問資源包括如下幾步:算法

    • 使用new操做符爲類型分配內存(這個過程調用了IL指令newobj)
    • 初始化內存,設置資源的初始狀態,來讓這個資源可用(類型的實力構造器負責初始化類型狀態)
    • 訪問類型成員使用資源
    • 摧毀資源狀態進行清理
    • 釋放內存

  在C#中,咱們的操做時基於CLR來完成的,咱們全部對象都是從託管堆對來分配內存。當進程初始化(咱們的程序)時,CLR會畫出一個地址空間區域來做爲託管堆。同時,CLR會維護一個指針NewObjPtr,這個指針指向下一個對象在託管堆中分配的地址。當這個區域被非垃圾的對象填滿後,CLR會分配更多的區域,這個過程將一直重複,直至整個進程的地址空間都被填滿。32位進程的地址空間爲1.5GB,64位進程爲8TB。(這裏順便提一下值類型的生命週期,值類型對象分配在線程棧上,當離開做於域時,自動銷燬。)數組

  C# new 操做符,會讓CLR執行如下步驟:網絡

    • 計算類型字段的所需字節數(這裏的字段是全部字段,包括基類繼承的)
    • 添加建立對象的額外所需字節數 (每個對象被初始化時,會建立類型對象指針和同步塊索引,32位程序爲8字節,64位程序16字節)
    • CLR檢查區域中空間是否足夠,假如足夠,在NewObjPtr指針位置放入對象。這時候,對象分配的字節會被清零,而後調用類型的構造器(計算字節,NewObjPtr指針會指向舊的位置加上這個字節的位置,爲下一個對象分配空間時候的位置),new操做符返回對象的引用。(例如在託管堆已經由A,B的狀況下新構建了一個C)

  關於GC數據結構

  當程序調用new操做符建立對象時候,假如沒有足夠的地址空間來分配該對象,CLR就會執行垃圾回收(調用GC)。CLR採用引用跟蹤算法,這種算法只關心引用類型的變量,避免了類型循環致使對象不能被回收的問題。引用類型包括類的靜態和實例字段,方法的局部變量和參數。全部的引用類型被稱之爲根。編輯器

  CLR開始執行GC時候,會暫停進程中全部線程(防止CLR執行檢查期間對象的狀態被更改);而後CLR遍歷託管堆中的全部對象,將同步塊索引中字段的一位設置爲0(標識全部對象都應該刪除),而後CLR檢查全部活動根,這些根引用了哪些對象。ide

  任何一個根引用了堆的對象,CLR會將對象的同步塊索引的位設置位1。而後再檢查對象中的跟,標記它們引用的對象。假如在遍歷過程當中發現對象被標記,就跳過這個對象,再也不從新檢查這個對象的字段,這樣避免了循環引用。函數

  應用程序中全部的活動跟都檢查完畢之後,這時候堆中的對象要麼被標記了(稱之爲可達,由活動跟在引用),要麼沒有被標記(相應稱爲不可達)。CLR將不可達的對象內存回收。將可達對象進行內存整理,使對象內存在託管堆中是連續的(壓縮過程當中CLR要從每一個根減去所引用對象在堆中的偏移字節數),以下圖所示:性能

二、GC高效的處理方式—代

  CLR的垃圾回收基於代。測試

  同時GC回收垃圾時,作出了下面幾點假設(能夠先記下,從下文中體會).net

  • 對象越新,生存期越短
  • 對象越來,生存期越長
  • 回收堆一部份內存,比回收整個堆要快   

 

  解釋下代:託管進程中有兩種內存堆,分別是本機堆託管堆,CLR在託管堆上面爲.net 的全部對象分配內存(託管堆又稱爲GC堆)。託管堆又分爲兩種,小對象堆大對象堆(LOH),小對象堆用來分配經常使用的資源對象內存(如類,數組等等),小對象堆的內存段進一步劃分爲3代,0代,1代,2代。(大對象堆用來分配一些大對象和非託管資源,咱們後文中專門來解釋)

 

   託管堆初始化時,不包含任何對象,當咱們聲明一個對象時,這個對象稱爲第0代對象。也就是說,第0代對象就是那些新構造的對象,並且垃圾回收器沒有檢查過的對象。例以下圖中,託管堆中分配了A,B,C,D,E5個對象,它們就是第0代對象。

  接下來隨着咱們不停地分配對象,第0代的堆內存使用完畢,且這隨着程序的流轉,C和E變得不可達,當咱們分配下一個內存F時,CLR就會執行一次垃圾回收。此時,C,E對象內存被回收掉,個人的ABD對象從第0代對象變爲第1代對象。這時候,垃圾回收結束,第0代不包含任何對象。以下圖所示:

  接下來隨着程序的運行,又在0代中分配了對象F G H I J K,1代對象中B變得不可達。接下來給對象L分配內存時內存不足,將執行垃圾回收。CLR會爲第0代對象和第 1代對象選擇預算,因爲第一代中的佔用內存遠少於預算,因此垃圾回收期只檢查第0代的對象(基越新的對象得到越短),由於第0代對象包含更多的垃圾可能性更大,能夠回收更多的內存。忽略了第一代中的對象,因此加快了垃圾回收速度。

 

 

  隨着垃圾回收的不斷進行,第1代的內存將不斷增長,當第1代對象的內存增加到佔用了佔用了所有預算(0代給新對象分配內存就要進行GC),此時,會進行第1代的垃圾回收,倖存下來的對象被分配的第2代中去。託管堆只支持3代(0,1,2)。超過85000字節的對象稱之爲大對象,直接由第2代分配內存。

  代給GC帶來的性能提高主要體如今沒必要遍歷託管堆中的每個對象。若是根或者對象引用了老一代的某個對象,垃圾回收期就能夠忽略老對象內部全部引用(CLR的特徵,引用跟蹤算法,同步索引塊中的一位標識),在更短的時間內構造好可達對象圖。假如老對象字段引用了新對象,則由JIT編輯器內部的一個機制(單獨解釋)讓垃圾回收期跳過。微軟官方性能測試,0代執行一次GC,花費時間很多過1毫秒。

  • JIT的機制是在對象引用字段發生變化時候,設置一個對應位標誌。這樣,下一次GC回收資源內存時候,會知道上一次GC事後,哪些老對象被寫入位標誌,這樣,只有位標誌發生變化(也就是老對象字段發生變化)時候,才檢查老對象是否引用第0代對象。

三、特殊類型的清理

  特殊類型:大多數對象只要分配內存就可使用。可是,還有部分對象須要分配本機資源(例如文件,網絡鏈接,套接字,互斥體),咱們稱這部分對象爲特殊類型的資源。

  特殊類型的回收過程和特色:包含本機資源的類型被GC時,GC在回收內存以前,須要將本機資源終結(Finalization)。當CLR斷定一個特殊類型的對象不可達時,對象將終結本身,釋放包裹的本機資源,而後由GC回收其內存。

  Object基類型定義了虛方法Finalize,GC斷定對象時垃圾後,調用對象的Finalize方法,這個方法通常以析構函數的形式出現。(ILSpy 反編譯後的析構函數代碼爲protected override Finalize)。

   特殊類型注意事項:

    1. Finalize執行在GC以後,因此特殊類型的對象不是立刻被GC回收,由於Finalize方法可能要訪問對象字段。這可能使對象提高到另外一級別的代,增長內存耗用。因此,儘可能避免引用類型的字段定義爲可終結對象。
    2. Finalize方法執行時無順序的。因此不要在Finalize方法中訪問定義了其餘Finalize方法的類型,由於另外一個類型對象可能已被終結。
    3. CLR用一個特殊的、高優先級專用線程調用Finalize方法避免死鎖。
    4. 自定義包含了本機資源的託管類型時要繼承自SafeHandle(派生自它保證本機資源在GC時被釋放)。

  控制包裝了本機資源類型對象的生存期:

      例如這裏咱們要往D盤的1.txt中寫入一部分文本,而後寫完後想把這個文件刪除,此時就會報 「System.IO.IOException:「文件「d:\1.txt」正由另外一進程使用,所以該進程沒法訪問此文件。」這樣一個異常,這是由於本機資源未被釋放(Finalize)。假如咱們想控制包裝本機資源的類型對象的生命週期,就要實現IDispose接口。(若是類型對象的其中一個字段實現了這個接口,那麼這個類型也就實現了Dispose模式。)而後咱們修改咱們的代碼,成功刪除文件。

   終結的內部實現原理:

    包裝了本機資源的對象被回收時,會調用Finalize方法。

    包裝了本機資源的對象建立的時候(定義了Finalize方法),在從堆中分配內存前,會將這個對象的指針添加到一個終結列表(由GC控制的內部數據結構)中。這個列表中的每一項,都指向一個定義了Finalize方法的對象,回收這些對象內存以前應該先調用它的Finalize方法(這裏注意,雖然Object也定義了Finalize方法,可是CLR會忽略它,只有重寫了Finalize方法的類型對象纔會加入到終結列表)。以下圖所示,C,E,F,I,J是定義了Finalize方法的類型對象,指向它們的指針被加入到終結列表中:

    

    垃圾回收開始進行,B,E,G,H,I,J被斷定爲垃圾,這時候垃圾回收器會掃描終結列表來查找這些對象的引用(這裏找到了E,I,J),而後把這些引用從終結列表中移除,附加到freachable隊列(也是GC的一個內部數據結構)。在freachable隊列中的每個引用都表明即將進行Finalize調用的對象。經歷過一輪GC後,堆內存以下所示:

    

    CLR使用一個高優先級的,專用的線程來調用Finalize方法,這個線程避免潛在的線程同步問題。當freachable隊列爲空時候,這個線程將休眠,freachable隊列出現記錄項,將喚醒這個線程。這樣來看,包裝了本機資源的託管對象至少要進行兩次GC才能回收它的內存,第一次由專用線程來執行Finalize方法,第二次才由GC回收這個對象的內存(大於2次是由於這些對象可能被提升到老的一代)。

四、手動監控和控制對象生命週期

  CLR爲每個AppDomain都提供了一個GC Handle table,容許程序監視或者控制對象的生命週期。這個表中的每一條記錄項都包含託管堆中一個對象的引用監視控制對象標誌。這裏注意一個類GCHandle和一個枚舉對象 GCHandleType。

  GCHandle調用Alloc方法時候,會掃描AppDomain的GC Handle table,查找一個可用的記錄項存儲對象的生命週期而且傳回給對象引用。GChandle的Target屬性,返回句柄表示的對象,以下圖所示:

  GC發生時候會使用GC Handle table,首先,GC將全部對象標識爲將要回收,掃描GC Handle table,全部GCHandleType爲Normal和Pinned對象標識爲根;而後查找GCHandleType爲Weak的項,若是引用了未標記的對象,那麼這個對象就是垃圾,且把這個項賦值爲null;GC繼續掃描中介列表,將無引用標識對象的引用放入freachable隊列;GC再掃描GC Handle table,查找GCHandleType 爲WeakTrackResurrection的記錄想,這些記錄想引用了未標記的對象(freachable隊列中)變爲垃圾,這些記錄項賦值爲Null。最後GC對內存進行壓縮。

相關文章
相關標籤/搜索