一:背景
寫這一篇的目的主要是因為.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 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();
}
可以清楚的看到就是在做 標記清除
且核心邏輯都在 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 -special
Lock
DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
0 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 type
0 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> bp coreclr!WKS::gc_heap::revisit_written_pages
0:008> g
coreclr!WKS::gc_heap::revisit_written_pages:
0:008> !t -special
Lock
DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
0 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 type
5 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 -special
Lock
DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
0 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 type
5 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,好了,本篇就聊這麼多,希望對你有幫助。