做者: Maoni Stephens (@maoni0) - 2015css
附: 關於垃圾回收的信息,能夠參照本文末尾資源章節裏引用的垃圾回收手冊一書。git
GC包含的兩個組件分別是內存分配器和垃圾收集器。內存分配器負責獲取更多的內存並在適當的時候觸發垃圾收集。垃圾收集器回收程序中再也不使用的對象的內存。程序員
有多種方法調用垃圾回收器,例如人工調用GC.Collect或者當終結線程在接收到表示低內存的異步通知時(調用)。github
內存分配器由執行引擎(EE)的內存分配輔助函數調用,並附上下列信息:緩存
GC不會區別對待不一樣的對象。請經過執行引擎來獲取對象的大小。服務器
基於對象的大小,GC將其分紅兩類:小對象(< 85,000字節)和大對象(>= 85,000字節)。原則上,大小對象均可以一樣處理,可是壓縮大對象耗費更加昂貴因此GC才這樣區分。架構
GC向內存分配器釋放內存是經過內存分配上下文完成的。內存上下文的大小有分配額度定義:異步
大對象不使用分配上下文和定額。一個大對象自己就比這些小內存區域(8k的定額)大了。並且,這些區域的優勢(下文討論)直適用於小對象。大對象就直接在堆區上分配了。ide
分配器的設計目標以下:函數
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將極其高效利用內存和儘可能避免編寫「託管代碼」的程序員的人工干預做爲奮鬥目標。高效是指:
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使用卡片來標註更老的代。卡片是由JIT輔助函數在分配操做時設置的。若是JIT輔助函數看到一個對象在短命區的範圍,而後設置包含卡片的字節來指示其來源位置。在收集短命區時,GC能夠在看堆上設置過的卡片並依次處理卡片對應的對象便可。
計劃階段模擬壓縮過程來決定最後的效果,若是壓縮效果很好那麼GC就會啓動壓縮,不然執行清理。
若是GC決定壓縮,其結果會移動對象,那麼對這些對象的引用必須更新。遷移階段須要處理全部指向所回收的代中的對象的引用。相比之下,而標註階段只處理存活對象所以不須要考慮弱引用(weak reference)。
這個階段很直觀,由於在計劃階段就已經計算對象應該移動的新地址,壓縮階段只須要將對象拷貝過去。
清理階段會查看兩個存活對象之間的空間。其爲這些空間建立閒置對象。相鄰的閒置對象會合並。它會將全部的閒置對象保存在 閒置對象列表(freelist)。
術語:
這些說明了一個後臺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(); }