一:背景
寫這一篇的目的主要是因為.NET領域內幾本關于闡述GC方面的書,都是純理論,所以懂得人自然懂,不懂得人也沒法親自驗證,這一篇我就用 windbg + 源碼
讓大家眼見為實。
二:為什么要引入后臺GC
1. 后臺GC到底解決了什么問題
解決什么問題得先說有什么問題,我們知道 阻塞版GC
有一個顯著得特點就是,在 GC 觸發期間,所有的用戶線程都被 暫停
了,這里的 暫停 是一個統稱,畫圖如下:

這種 STW(Stop The World) 模式相信大家都習以為常了,但這里有一個很大的問題,不管當前 GC 是臨時代還是全量,還是壓縮或者標記,all in 全凍結,這種簡單粗暴的做法肯定是不可取的,也是 后臺GC
引入的先決條件。
那 后臺GC 到底解決了什么問題?
解決在 FullGC 模式下的
標記清除
回收期間,放飛用戶線程。
雖然這是一個很好的 Idea,但復雜度絕對上了幾個檔次。
三:后臺GC 詳解
1. 后臺 GC代碼 骨架圖
源碼面前,了無秘密,在coreclr 項目的 garbage-collection.md
文件中,描述了 后臺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?workdo_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?upgc1();}}gc1(){background_mark_phase();background_sweep();}
可以清楚的看到就是在做 標記清除
且核心邏輯都在 background_mark_phase()
函數中,實現了標記的三個階段:?1.初始標記
, 2.并發標記
,3.最終標記
, 其中 并發標記 階段,用戶線程是正常運行的,實現了將原來整個暫停 優化到了 2個小暫停。
2. 流程圖分析
為了方便說明,將三階段畫個圖如下:

特別聲明:階段2的重啟是在
background_sweep()
方法中,而不是最終標記(background_mark_phase)
階段。
初始標記
這個階段用戶線程處于暫停狀態,bgc 要做的事情就是從 線程棧
和 終結器隊列
中尋找用戶根實現引用圖遍歷,然后再讓所有用戶線程啟動,簡化后的代碼如下:
void?gc_heap::background_mark_phase()
{dprintf(3,?("BGC:?stack?marking"));GCScan::GcScanRoots(background_promote_callback,max_generation,?max_generation,&sc);dprintf(3,?("BGC:?finalization?marking"));finalize_queue->GcScanRoots(background_promote_callback,?heap_number,?0);restart_vm();
}
接下來怎么驗證 階段1
是暫停狀態呢?為了方便講述,先上一段測試代碼:
internal?class?Program{static?List<string>?list?=?new?List<string>();static?void?Main(string[]?args){Debugger.Break();for?(int?i?=?0;?i?<?int.MaxValue;?i++){list.Add(String.Join(",",?Enumerable.Range(0,?100)));if?(i?%?10?==?0)?list.RemoveAt(0);}}}
然后用 windbg 在 background_mark_phase 函數下一個斷點:bp coreclr!WKS::gc_heap::background_mark_phase
即可。
0:009>?bp?coreclr!WKS::gc_heap::background_mark_phase
0:009>?g
Breakpoint?1?hit
coreclr!WKS::gc_heap::background_mark_phase:
00007ff9`e7bf73f4?488bc4??????????mov?????rax,rsp
0:008>?!t?-specialLock??DBG???ID?????OSID?ThreadOBJ???????????State?GC?Mode?????GC?Alloc?Context??????????????????Domain???????????Count?Apt?Exception0????1?????55d8?00000000006336B0????2a020?Preemptive??0000000000000000:0000000000000000?000000000062d650?-00001?MTA?(GC)?6????2?????568c?0000000000662F40????21220?Preemptive??0000000000000000:0000000000000000?000000000062d650?-00001?Ukn?(Finalizer)?8????4?????5730?0000000000676A90????21220?Preemptive??0000000000000000:0000000000000000?000000000062d650?-00001?Ukn?OSID?Special?thread?type0?55d8?SuspendEE?5?5688?DbgHelper?6?568c?Finalizer?8?5730?GC
可以清楚的看到,0號線程顯示了 SuspendEE 字樣,表示此時所有托管線程處于凍結狀態。
并發標記
這個階段就是各玩各的,用戶線程在正常執行,bgc在后臺進一步標記,因為是并行,所以存在 bgc 已標記好的對象引用關系被 用戶線程
破壞,所以 bgc 用 reset_write_watch
函數借助 windows 的內存頁監控,目的就是把那些臟頁找出來,在下一個階段來修正,簡化后的代碼如下:
void?gc_heap::background_mark_phase()
{disable_preemptive(true);//臟頁監控reset_write_watch(TRUE);revisit_written_pages(TRUE,?TRUE);dprintf(3,?("BGC:?handle?table?marking"));GCScan::GcScanHandles(background_promote,max_generation,?max_generation,&sc);disable_preemptive(false);
}
要想驗證此時的用戶線程
是放飛的,可以在 revisit_written_pages
函數下一個斷點即可,使用命令:bp coreclr!WKS::gc_heap::revisit_written_pages
。
0:008>?!t?-specialLock??DBG???ID?????OSID?ThreadOBJ???????????State?GC?Mode?????GC?Alloc?Context??????????????????Domain???????????Count?Apt?Exception0????1?????55d8?00000000006336B0????2a020?Cooperative?000000000D1FD920:000000000D1FE120?000000000062d650?-00001?MTA?6????2?????568c?0000000000662F40????21220?Preemptive??0000000000000000:0000000000000000?000000000062d650?-00001?Ukn?(Finalizer)?8????4?????5730?0000000000676A90????21220?Cooperative?0000000000000000:0000000000000000?000000000062d650?-00001?Ukn?OSID?Special?thread?type5?5688?DbgHelper?6?568c?Finalizer?8?5730?GC
看到沒有,那個 SuspendEE
神奇的消失了,而且 0 號線程的 GC 模式也改成了 Cooperative
,表示可允許操控 托管堆。
最終標記
等 bgc 在后臺做的差不多了,就可以再來一次 SupendEE
,將 并發標記
期間由用戶線程造成的臟引用進行最終一次修正,修正的數據來源就是監控到的 Windows臟頁
,代碼就不上了,我們聊下怎么去驗證階段二又回到了 SuspendEE 狀態?可以在 background_sweep()
函數下一個斷點, 命令: bp coreclr!WKS::gc_heap::background_sweep
。
0:000>?bp?coreclr!WKS::gc_heap::background_sweep
0:000>?g
coreclr!WKS::gc_heap::background_sweep:
00007ff9`e7b7a2e0?4053????????????push????rbx
0:008>?!t?-specialLock??DBG???ID?????OSID?ThreadOBJ???????????State?GC?Mode?????GC?Alloc?Context??????????????????Domain???????????Count?Apt?Exception0????1?????55d8?00000000006336B0????2a020?Preemptive??0000000000000000:0000000000000000?000000000062d650?-00001?MTA?6????2?????568c?0000000000662F40????21220?Preemptive??0000000000000000:0000000000000000?000000000062d650?-00001?Ukn?(Finalizer)?8????4?????5730?0000000000676A90????21220?Preemptive??0000000000000000:0000000000000000?000000000062d650?-00001?Ukn?(GC)?OSID?Special?thread?type5?5688?DbgHelper?6?568c?Finalizer?8?5730?GC?SuspendEE
哈哈,可以看到那個 SuspendEE
又回來了。
3. 后臺GC 只會在 fullGC 模式下嗎?
這是最后一個要讓大家眼見為實的問題,在gc觸發期間,內部會維護一個 gc_mechanisms
結構體,其中就記錄了當前 GC 觸發的種種信息,可以用 windbg 把它導出來看看便知。
0:008>?x?coreclr!*settings*
00007ff9`e7f82e90?coreclr!WKS::gc_heap::settings?=?class?WKS::gc_mechanisms
0:008>?dt?coreclr!WKS::gc_heap::settings?00007ff9`e7f82e90+0x000?gc_index?????????:?0xb3+0x008?condemned_generation?:?0n2+0x00c?promotion????????:?0n1+0x010?compaction???????:?0n0+0x014?loh_compaction???:?0n0+0x018?heap_expansion???:?0n0+0x01c?concurrent???????:?1+0x020?demotion?????????:?0n0+0x024?card_bundles?????:?0n1+0x028?gen0_reduction_count?:?0n0+0x02c?should_lock_elevation?:?0n0+0x030?elevation_locked_count?:?0n0+0x034?elevation_reduced?:?0n0+0x038?minimal_gc???????:?0n0+0x03c?reason???????????:?0?(?reason_alloc_soh?)+0x040?pause_mode???????:?1?(?pause_interactive?)+0x044?found_finalizers?:?0n1+0x048?background_p?????:?0n0+0x04c?b_state??????????:?0?(?bgc_not_in_process?)+0x050?allocations_allowed?:?0n1+0x054?stress_induced???:?0n0+0x058?entry_memory_load?:?0x49+0x060?entry_available_physical_mem?:?0x00000001`0a50d000+0x068?exit_memory_load?:?0
從 condemned_generation=2
可知當前觸發的是 2 代GC,原因是代滿了 reason : 0 ( reason_alloc_soh )
。
四:總結
看的再多還不如實操一遍,如果覺得手工編譯 coreclr 源碼麻煩,可以考慮下 windbg,好了,本篇就聊這么多,希望對你有幫助。