CLR垃圾回收的設計

做者: Maoni Stephens (@maoni0) - 2015css

附: 關於垃圾回收的信息,能夠參照本文末尾資源章節裏引用的垃圾回收手冊一書。git

組件架構

GC包含的兩個組件分別是內存分配器和垃圾收集器。內存分配器負責獲取更多的內存並在適當的時候觸發垃圾收集。垃圾收集器回收程序中再也不使用的對象的內存。程序員

有多種方法調用垃圾回收器,例如人工調用GC.Collect或者當終結線程在接收到表示低內存的異步通知時(調用)。github

內存分配器的設計

內存分配器由執行引擎(EE)的內存分配輔助函數調用,並附上下列信息:緩存

  • 請求的大小
  • 線程分配上下文
  • 一個說明該對象是否可終結的標識。

GC不會區別對待不一樣的對象。請經過執行引擎來獲取對象的大小。服務器

基於對象的大小,GC將其分紅兩類:小對象(< 85,000字節)和大對象(>= 85,000字節)。原則上,大小對象均可以一樣處理,可是壓縮大對象耗費更加昂貴因此GC才這樣區分。架構

GC向內存分配器釋放內存是經過內存分配上下文完成的。內存上下文的大小有分配額度定義:異步

  • 內存分配上下文(Allocation contexts)是線程專用的堆區(heap segment)上小一點的區域。在單處理器(即一個邏輯處理器)機器上,使用單上下文,也就是第0代內存分配器上下文。
  • 內存分配定額(Allocation quantum)是分配器在一個內存分配上下文中執行對象分配時要求更多內存時的分配定額。這個定額一般是8k,而託管對象的平均大小大約是35個字節,這樣在一個分配額度裏能夠知足不少對象的分配請求。

大對象不使用分配上下文和定額。一個大對象自己就比這些小內存區域(8k的定額)大了。並且,這些區域的優勢(下文討論)直適用於小對象。大對象就直接在堆區上分配了。ide

分配器的設計目標以下:函數

  • 在適當的時候觸發GC: 分配器在超出分配預算(由收集器設置的一個閾值)時,或者分配器沒法在堆區上分配時觸發GC。後文會詳細介紹分配預算和託管堆區。
  • 保留對象的本地性: 在同一個堆區上分配的對象,保存它們的虛擬內存地址也是挨着的。
  • 提升緩存的效率: 分配器以 分配定額 爲單位分配內存,而按不是一個個對象分配。其將這些內存置零來便CPU的緩存提早作準備,由於隨後立刻就有對象在這塊內存中建立。分配定額一般是8k。
  • 提升鎖的效率: 內存分配上下文的線程關聯性和定額保證有且只有一個線程寫入指定的定額分配的內存。結果就是隻要當前的內存分配上下文沒有用光的話,對象分配是不須要加鎖的。
  • 內存完整性: 對於新建立的對象,GC老是將內存置零,以防止對象引用了隨機的內存位置。
  • 保持堆的可遍歷性: 分配器保證定額的剩餘內存是一個空閒對象。例如,若是定額裏只剩下30個字節,而下一個要分配的對象大小是40字節,分配器爲這30個字節建立一個空閒對象並申請一個新的分配定額。

內存分配 APIs

Object* GCHeap::Alloc(size_t size, DWORD flags); Object* GCHeap::Alloc(alloc_context* acontext, size_t size, DWORD flags);

上面的函數能夠用來分配大對象和小對象。也有一個對象能夠直接在大對象堆裏分配內存:

Object* GCHeap::AllocLHeap(size_t size, DWORD flags);

收集器的設計

GC的目標

GC將極其高效利用內存和儘可能避免編寫「託管代碼」的程序員的人工干預做爲奮鬥目標。高效是指:

  • GC應該足夠頻繁發生,以免託管堆上有大量(按比率或者絕對值)已分配的但無用的對象(垃圾),致使非必要的使用內存。
  • GC應該儘可能不頻繁發生,避免佔有有用的CPU時間,哪怕在低內存致使的頻繁GC。
  • GC應該有高效產出。若是GC只回收了一小部份內存,那麼GC(包括其使用的CPU週期)都是浪費的。
  • 每次GC應該儘可能快。不少工做負荷要求低延遲。
  • 託管代碼程序員應該不須要知道GC的太多細節而能達到高效的內存使用率。
  • GC應該自我調整以知足不一樣的內存使用模式。

託管堆的邏輯形式

CLR GC是一個分代收集器,即對象是邏輯劃分紅幾個代的。當第 N 代收集完畢後,剩下來的存活對象則被標識爲第 N+1 代。這個過程被稱做升級。也有異常狀況咱們決定降級或者不升級。

小對象堆被分紅3代:gen0, gen1和gen2。大對象只有一代 - gen3。gen0和gen1被稱爲短命代(對象存活的時間不長)。

對於小對象堆,代的數字表示它的年齡 - gen0屬於最年輕的一代。這不是說gen0裏全部的對象比gen1或gen2中任意一個對象年輕。後文會提到一些異常情形。收集一代是指收集這一代和全部比其年輕的代。

原則上大對象可使用跟小對象相同的辦法處理,可是壓縮大對象的代價很高,才區別對待。出於性能的考量,大對象只有一代並且老是跟gen2一塊兒收集。gen2和gen3能夠很大,可是收集短命代(gen0和gen1)的成本有限制。

內存分配是在最年輕的代發生的 - 對小對象來講老是gen0,而對大對象來講是gen3,由於只有一代。

託管堆的物理形式

託管堆是一系列的託管堆區。一個託管堆區是GC從操做系統那裏申請的一個連續的內存區域。堆區被分紅大小對象區,對應大小對象。每一個堆的堆區都鏈在一塊兒。至少有一個小對象堆區和一個大對象堆區 - 用來爲加載CLR而保留。

每一個小對象堆老是隻有一個短命區,用來保存gen0和gen1代。這個堆區有可能包含gen2的對象。除了短命區之外,有可能有零個、一個或多個額外的堆區,用來做爲gen2堆區並保存gen2對象。

在大對象堆上有一個或多個堆區。

堆區的使用是從低地址開始到高地址,即堆區裏低地址對象的時間比高地址對象久。一樣下文也有一些異常狀況。

堆區能夠按需申請,若是其不包含存活對象就會被刪除,可是堆上初始的第一個堆區一直都在。對於每一個堆,一次申請一個堆區,這個在給小對象作垃圾回收時和建立大對象時發生。這樣作有更好的性能,由於大對象只會跟gen2一塊兒回收(執行起來代價更高)。

堆區按照申請的順序連接在一塊兒。鏈表上最後一個堆區永遠是短命區。回收過的堆區(沒有存活對象)會被複用而不是直接被刪除,也就是變成新的短命區。堆區複用只發生在小對象堆。每當分配一個大對象,會考慮整個大對象堆。而小對象的分配只考慮短命區。

分配預算

分配預算是跟每一個代關聯的邏輯概念。這是代裏的一個大小限制用來在超出時觸發一個GC。

預算是設置在代上基於該代對象存活率的一個屬性。若是存活率高,那麼預算就會大一些,這樣在下一次GC的時候銷燬的對象和存活的對象有一個更好的比率。

肯定回收哪一代

當觸發一個GC時,GC必須決定回收哪一代。除了分配預算之外還要考慮如下幾個因素:

  • 代上碎片狀況 - 若是代上的內存碎片很嚴重,那麼在這個代上回收產量可能很高。
  • 若是機器上內存負荷很大,那麼GC會更積極的回收來產生更多的可用空間。這對避免沒必要要的頁面調度很重要。
  • 若是短命堆區沒有空間的話,GC會更積極的回收短命對象(更多的gen1回收)來避免申請一個新的堆區。

GC的流程

標註階段

標註階段的目標是找出全部存活的對象。

按代回收的好處是隻須要考慮堆的一部分而不是每次都處理全部對象。當回收短命代時,GC只須要找到這一個代裏存活的對象,這些信息由執行引擎上報。除了執行引擎可能引用對象之外,更老一代的對象也可能會引用新一代的對象。

對於GC使用卡片來標註更老的代。卡片是由JIT輔助函數在分配操做時設置的。若是JIT輔助函數看到一個對象在短命區的範圍,而後設置包含卡片的字節來指示其來源位置。在收集短命區時,GC能夠在看堆上設置過的卡片並依次處理卡片對應的對象便可。

計劃階段

計劃階段模擬壓縮過程來決定最後的效果,若是壓縮效果很好那麼GC就會啓動壓縮,不然執行清理。

遷移階段

若是GC決定壓縮,其結果會移動對象,那麼對這些對象的引用必須更新。遷移階段須要處理全部指向所回收的代中的對象的引用。相比之下,而標註階段只處理存活對象所以不須要考慮弱引用(weak reference)。

壓縮階段

這個階段很直觀,由於在計劃階段就已經計算對象應該移動的新地址,壓縮階段只須要將對象拷貝過去。

清理階段

清理階段會查看兩個存活對象之間的空間。其爲這些空間建立閒置對象。相鄰的閒置對象會合並。它會將全部的閒置對象保存在 閒置對象列表(freelist)

代碼流程

術語:

  • WKS GC: 工做站 GC.
  • SRV GC: 服務器 GC

功能行爲

WKS GC並關閉了並行GC

  1. 用戶線程用完了分配預算並觸發一個GC。
  2. GC調用SuspendEE來暫停託管線程。
  3. GC決定回收哪一代。
  4. 執行標註階段。
  5. 執行計劃階段並決定是否要執行壓縮。
  6. 若是要壓縮則執行遷移和壓縮過程。不然執行清理過程。
  7. GC調用RestartEE來恢復託管線程。
  8. 用戶線程恢復執行。

WKS GC並打開了並行GC

這些說明了一個後臺GC是如何實施的:

  1. 用戶線程用完了分配預算並觸發一個GC。
  2. GC調用SuspendEE來暫停託管線程。
  3. GC決定是否須要後臺GC運行。
  4. 若是須要後臺GC,喚醒它。後臺GC線程調用RestartEE來恢復託管線程的執行。
  5. 託管線程在後臺GC執行的同時運行並分配內存。
  6. 用戶線程可能會用完分配預算並觸發一個短命代GC(咱們稱之爲前臺GC)。這個過程跟「WKS GC並關閉了並行GC」同樣。
  7. 後臺GC再次調用SuspendEE來完成標註並調用RestartEE來在用戶線程運行的同時並行執行清理階段。
  8. 後臺GC處理完畢.

SVR GC並關閉了並行GC

  1. 用戶線程用完了分配預算並觸發一個GC。
  2. 服務器GC線程被喚醒冰調用SuspendEE來暫停託管線程。
  3. 服務器GC線程執行GC工做(與WKS GC並關閉了並行GC同樣).
  4. 服務器GC線程調用RestartEE來恢復託管線程。
  5. 用戶線程恢復執行。

SVR GC並打開了並行GC

這個場景跟WKS GC並打開了並行GC同樣,除了在服務器GC線程上沒有後臺GC。

物理架構

這個章節用來幫助你理解代碼過程。

用戶線程用完定額以後,經過try_allocate_more_space申請新定額。

try_allocate_more_space在須要觸發GC時調用GarbageCollectGeneration。

假如WKS GC並關閉了並行GC,GarbageCollectGeneration在觸發GC的用戶線程上執行,代碼過程以下:

GarbageCollectGeneration()
 {
     SuspendEE();
     garbage_collect();
     RestartEE();
 }

 garbage_collect()
 {
     generation_to_condemn();
     gc1();
 }

 gc1()
 {
     mark_phase();
     plan_phase();
 }

 plan_phase()
 {
     // actual plan phase work to decide to // compact or not if (compact) { relocate_phase(); compact_phase(); } else make_free_lists(); }

假如WKS GC並打開了並行GC(默認狀況),後臺GC的代碼過程以下:

GarbageCollectGeneration()
 {
     SuspendEE();
     garbage_collect();
     RestartEE();
 }

 garbage_collect()
 {
     generation_to_condemn();
     // decide to do a background GC // wake up the background GC thread to do the work do_background_gc(); } do_background_gc() { init_background_gc(); start_c_gc (); //wait until restarted by the BGC. wait_to_proceed(); } bgc_thread_function() { while (1) { // wait on an event // wake up gc1(); } } gc1() { background_mark_phase(); background_sweep(); }

資料

相關文章
相關標籤/搜索