聊一聊 C# 后台GC 到底是怎么回事?
<h2 id="一背景">一:背景</h2><p>写这一篇的目的主要是因为.NET领域内几本关于阐述GC方面的书,都是纯理论,所以懂得人自然懂,不懂得人也没法亲自验证,这一篇我就用 <code>windbg + 源码</code> 让大家眼见为实。</p>
<h2 id="二为什么要引入后台gc">二:为什么要引入后台GC</h2>
<h3 id="1-后台gc到底解决了什么问题">1. 后台GC到底解决了什么问题</h3>
<p>解决什么问题得先说有什么问题,我们知道 <code>阻塞版GC</code> 有一个显著得特点就是,在 GC 触发期间,所有的用户线程都被 <code>暂停</code>了,这里的 暂停 是一个统称,画图如下:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f3f1eb5d86994daf92677ace123b6f0b~tplv-k3u1fbpfcp-zoom-1.image" alt="" loading="lazy"></p>
<p>这种 STW(Stop The World) 模式相信大家都习以为常了,但这里有一个很大的问题,不管当前 GC 是临时代还是全量,还是压缩或者标记,all in 全冻结,这种简单粗暴的做法肯定是不可取的,也是 <code>后台GC</code> 引入的先决条件。</p>
<p>那 后台GC 到底解决了什么问题?</p>
<blockquote>
<p>解决在 FullGC 模式下的 <code>标记清除</code> 回收期间,放飞用户线程。</p>
</blockquote>
<p>虽然这是一个很好的 Idea,但复杂度绝对上了几个档次。</p>
<h2 id="三后台gc-详解">三:后台GC 详解</h2>
<h3 id="1-后台-gc代码-骨架图">1. 后台 GC代码 骨架图</h3>
<p>源码面前,了无秘密,在coreclr 项目的 <code>garbage-collection.md</code> 文件中,描述了 后台GC 的代码流程图。</p>
<pre><code class="language-C++">
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();
}
</code></pre>
<p>可以清楚的看到就是在做 <code>标记清除</code> 且核心逻辑都在 <code>background_mark_phase()</code> 函数中,实现了标记的三个阶段:<code>1.初始标记</code>, <code>2.并发标记</code> ,<code>3.最终标记</code> , 其中 并发标记 阶段,用户线程是正常运行的,实现了将原来整个暂停 优化到了 2个小暂停。</p>
<h3 id="2-流程图分析">2. 流程图分析</h3>
<p>为了方便说明,将三阶段画个图如下:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bca12dc57b8a4455814e93e17d18e903~tplv-k3u1fbpfcp-zoom-1.image" alt="" loading="lazy"></p>
<blockquote>
<p>特别声明:阶段2的重启是在 <code>background_sweep()</code> 方法中,而不是 <code>最终标记(background_mark_phase)</code> 阶段。</p>
</blockquote>
<ol>
<li>初始标记</li>
</ol>
<p>这个阶段用户线程处于暂停状态,bgc 要做的事情就是从 <code>线程栈</code> 和 <code>终结器队列</code> 中寻找用户根实现引用图遍历,然后再让所有用户线程启动,简化后的代码如下:</p>
<pre><code class="language-C++">
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();
}
</code></pre>
<p>接下来怎么验证 <code>阶段1</code> 是暂停状态呢? 为了方便讲述,先上一段测试代码:</p>
<pre><code class="language-C#">
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);
}
}
}
</code></pre>
<p>然后用 windbg 在 background_mark_phase 函数下一个断点:<code>bp coreclr!WKS::gc_heap::background_mark_phase</code> 即可。</p>
<pre><code class="language-C++">
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 Preemptive0000000000000000:0000000000000000 000000000062d650 -00001 MTA (GC)
6 2 568c 0000000000662F40 21220 Preemptive0000000000000000:0000000000000000 000000000062d650 -00001 Ukn (Finalizer)
8 4 5730 0000000000676A90 21220 Preemptive0000000000000000:0000000000000000 000000000062d650 -00001 Ukn
OSID Special thread type
0 55d8 SuspendEE
5 5688 DbgHelper
6 568c Finalizer
8 5730 GC
</code></pre>
<p>可以清楚的看到,0号线程显示了 <strong>SuspendEE</strong> 字样,表示此时所有托管线程处于冻结状态。</p>
<ol start="2">
<li>并发标记</li>
</ol>
<p>这个阶段就是各玩各的,用户线程在正常执行,bgc在后台进一步标记,因为是并行,所以存在 bgc 已标记好的对象引用关系被 <code>用户线程</code> 破坏,所以 bgc 用 <code>reset_write_watch</code> 函数借助 windows 的内存页监控,目的就是把那些脏页找出来,在下一个阶段来修正,简化后的代码如下:</p>
<pre><code class="language-C++">
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);
}
</code></pre>
<p>要想验证此时的<code>用户线程</code>是放飞的,可以在 <code>revisit_written_pages</code> 函数下一个断点即可,使用命令:<code>bp coreclr!WKS::gc_heap::revisit_written_pages</code> 。</p>
<pre><code class="language-C#">
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 Preemptive0000000000000000: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
</code></pre>
<p>看到没有,那个 <code>SuspendEE</code> 神奇的消失了,而且 0 号线程的 GC 模式也改成了 <code>Cooperative</code>,表示可允许操控 托管堆。</p>
<ol start="3">
<li>最终标记</li>
</ol>
<p>等 bgc 在后台做的差不多了,就可以再来一次 <code>SupendEE</code>,将 <code>并发标记</code> 期间由用户线程造成的脏引用进行最终一次修正,修正的数据来源就是监控到的 <code>Windows脏页</code>,代码就不上了,我们聊下怎么去验证阶段二又回到了 SuspendEE 状态?可以在 <code>background_sweep()</code> 函数下一个断点, 命令: <code>bp coreclr!WKS::gc_heap::background_sweep</code> 。</p>
<pre><code class="language-C++">
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 Preemptive0000000000000000:0000000000000000 000000000062d650 -00001 MTA
6 2 568c 0000000000662F40 21220 Preemptive0000000000000000:0000000000000000 000000000062d650 -00001 Ukn (Finalizer)
8 4 5730 0000000000676A90 21220 Preemptive0000000000000000:0000000000000000 000000000062d650 -00001 Ukn (GC)
OSID Special thread type
5 5688 DbgHelper
6 568c Finalizer
8 5730 GC SuspendEE
</code></pre>
<p>哈哈,可以看到那个 <code>SuspendEE</code> 又回来了。</p>
<h3 id="3-后台gc-只会在-fullgc-模式下吗">3. 后台GC 只会在 fullGC 模式下吗?</h3>
<p>这是最后一个要让大家眼见为实的问题,在gc触发期间,内部会维护一个 <code>gc_mechanisms</code> 结构体,其中就记录了当前 GC 触发的种种信息,可以用 windbg 把它导出来看看便知。</p>
<pre><code class="language-C++">
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
</code></pre>
<p>从 <code>condemned_generation=2</code> 可知当前触发的是 2 代GC,原因是代满了 <code>reason : 0 ( reason_alloc_soh )</code> 。</p>
<h2 id="四总结">四:总结</h2>
<p>看的再多还不如实操一遍,如果觉得手工编译 coreclr 源码麻烦,可以考虑下 windbg,好了,本篇就聊这么多,希望对你有帮助。</p>
<img src="https://images.cnblogs.com/cnblogs_com/huangxincheng/345039/o_210929020104最新消息优惠促销公众号关注二维码.jpg" width="700" height="300" alt="图片名称" align="center"><br><br>
来源:https://www.cnblogs.com/huangxincheng/p/16492698.html
頁:
[1]