聊一聊 C# 后台GC 到底是怎么回事?

打印 上一主题 下一主题

主题 908|帖子 908|积分 2724

一:背景

写这一篇的目的主要是因为.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 的代码流程图。
  1.      GarbageCollectGeneration()
  2.      {
  3.          SuspendEE();
  4.          garbage_collect();
  5.          RestartEE();
  6.      }
  7.      
  8.      garbage_collect()
  9.      {
  10.          generation_to_condemn();
  11.          // decide to do a background GC
  12.          // wake up the background GC thread to do the work
  13.          do_background_gc();
  14.      }
  15.      
  16.      do_background_gc()
  17.      {
  18.          init_background_gc();
  19.          start_c_gc ();
  20.      
  21.          //wait until restarted by the BGC.
  22.          wait_to_proceed();
  23.      }
  24.      
  25.      bgc_thread_function()
  26.      {
  27.          while (1)
  28.          {
  29.              // wait on an event
  30.              // wake up
  31.              gc1();
  32.          }
  33.      }
  34.      
  35.      gc1()
  36.      {
  37.          background_mark_phase();
  38.          background_sweep();
  39.      }
复制代码
可以清楚的看到就是在做 标记清除 且核心逻辑都在 background_mark_phase() 函数中,实现了标记的三个阶段:  1.初始标记, 2.并发标记 ,3.最终标记 , 其中 并发标记 阶段,用户线程是正常运行的,实现了将原来整个暂停 优化到了 2个小暂停。
2. 流程图分析

为了方便说明,将三阶段画个图如下:

特别声明:阶段2的重启是在 background_sweep() 方法中,而不是 最终标记(background_mark_phase) 阶段。

  • 初始标记
这个阶段用户线程处于暂停状态,bgc 要做的事情就是从 线程栈 和 终结器队列 中寻找用户根实现引用图遍历,然后再让所有用户线程启动,简化后的代码如下:
  1. void gc_heap::background_mark_phase()
  2. {
  3.         dprintf(3, ("BGC: stack marking"));
  4.         GCScan::GcScanRoots(background_promote_callback,
  5.                 max_generation, max_generation,
  6.                 &sc);
  7.         dprintf(3, ("BGC: finalization marking"));
  8.         finalize_queue->GcScanRoots(background_promote_callback, heap_number, 0);
  9.         restart_vm();
  10. }
复制代码
接下来怎么验证 阶段1 是暂停状态呢? 为了方便讲述,先上一段测试代码:
  1.     internal class Program
  2.     {
  3.         static List<string> list = new List<string>();
  4.         static void Main(string[] args)
  5.         {
  6.             Debugger.Break();
  7.             for (int i = 0; i < int.MaxValue; i++)
  8.             {
  9.                 list.Add(String.Join(",", Enumerable.Range(0, 100)));
  10.                 if (i % 10 == 0) list.RemoveAt(0);
  11.             }
  12.         }
  13.     }
复制代码
然后用 windbg 在 background_mark_phase 函数下一个断点:bp coreclr!WKS::gc_heap::background_mark_phase 即可。
  1. 0:009> bp coreclr!WKS::gc_heap::background_mark_phase
  2. 0:009> g
  3. Breakpoint 1 hit
  4. coreclr!WKS::gc_heap::background_mark_phase:
  5. 00007ff9`e7bf73f4 488bc4          mov     rax,rsp
  6. 0:008> !t -special
  7.                                                                                                             Lock  
  8. DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
  9.    0    1     55d8 00000000006336B0    2a020 Preemptive  0000000000000000:0000000000000000 000000000062d650 -00001 MTA (GC)
  10.    6    2     568c 0000000000662F40    21220 Preemptive  0000000000000000:0000000000000000 000000000062d650 -00001 Ukn (Finalizer)
  11.    8    4     5730 0000000000676A90    21220 Preemptive  0000000000000000:0000000000000000 000000000062d650 -00001 Ukn
  12.           OSID Special thread type
  13.         0 55d8 SuspendEE
  14.         5 5688 DbgHelper
  15.         6 568c Finalizer
  16.         8 5730 GC
复制代码
可以清楚的看到,0号线程显示了 SuspendEE 字样,表示此时所有托管线程处于冻结状态。

  • 并发标记
这个阶段就是各玩各的,用户线程在正常执行,bgc在后台进一步标记,因为是并行,所以存在 bgc 已标记好的对象引用关系被 用户线程 破坏,所以 bgc 用 reset_write_watch 函数借助 windows 的内存页监控,目的就是把那些脏页找出来,在下一个阶段来修正,简化后的代码如下:
  1. void gc_heap::background_mark_phase()
  2. {
  3.         disable_preemptive(true);
  4.        
  5.     //脏页监控
  6.         reset_write_watch(TRUE);
  7.         revisit_written_pages(TRUE, TRUE);
  8.         dprintf(3, ("BGC: handle table marking"));
  9.         GCScan::GcScanHandles(background_promote,
  10.                 max_generation, max_generation,
  11.                 &sc);
  12.        
  13.     disable_preemptive(false);
  14. }
复制代码
要想验证此时的用户线程是放飞的,可以在 revisit_written_pages 函数下一个断点即可,使用命令:bp coreclr!WKS::gc_heap::revisit_written_pages 。
  1. 0:008> bp coreclr!WKS::gc_heap::revisit_written_pages
  2. 0:008> g
  3. coreclr!WKS::gc_heap::revisit_written_pages:
  4. 0:008> !t -special
  5.                                                                                                             Lock  
  6. DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
  7.    0    1     55d8 00000000006336B0    2a020 Cooperative 000000000D1FD920:000000000D1FE120 000000000062d650 -00001 MTA
  8.    6    2     568c 0000000000662F40    21220 Preemptive  0000000000000000:0000000000000000 000000000062d650 -00001 Ukn (Finalizer)
  9.    8    4     5730 0000000000676A90    21220 Cooperative 0000000000000000:0000000000000000 000000000062d650 -00001 Ukn
  10.           OSID Special thread type
  11.         5 5688 DbgHelper
  12.         6 568c Finalizer
  13.         8 5730 GC
复制代码
看到没有,那个 SuspendEE 神奇的消失了,而且 0 号线程的 GC 模式也改成了 Cooperative,表示可允许操控 托管堆。

  • 最终标记
等 bgc 在后台做的差不多了,就可以再来一次 SupendEE,将 并发标记 期间由用户线程造成的脏引用进行最终一次修正,修正的数据来源就是监控到的 Windows脏页,代码就不上了,我们聊下怎么去验证阶段二又回到了 SuspendEE 状态?可以在 background_sweep() 函数下一个断点, 命令: bp coreclr!WKS::gc_heap::background_sweep 。
  1. 0:000> bp coreclr!WKS::gc_heap::background_sweep
  2. 0:000> g
  3. coreclr!WKS::gc_heap::background_sweep:
  4. 00007ff9`e7b7a2e0 4053            push    rbx
  5. 0:008> !t -special
  6.                                                                                                             Lock  
  7. DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
  8.    0    1     55d8 00000000006336B0    2a020 Preemptive  0000000000000000:0000000000000000 000000000062d650 -00001 MTA
  9.    6    2     568c 0000000000662F40    21220 Preemptive  0000000000000000:0000000000000000 000000000062d650 -00001 Ukn (Finalizer)
  10.    8    4     5730 0000000000676A90    21220 Preemptive  0000000000000000:0000000000000000 000000000062d650 -00001 Ukn (GC)
  11.           OSID Special thread type
  12.         5 5688 DbgHelper
  13.         6 568c Finalizer
  14.         8 5730 GC SuspendEE
复制代码
哈哈,可以看到那个 SuspendEE 又回来了。
3. 后台GC 只会在 fullGC 模式下吗?

这是最后一个要让大家眼见为实的问题,在gc触发期间,内部会维护一个 gc_mechanisms 结构体,其中就记录了当前 GC 触发的种种信息,可以用 windbg 把它导出来看看便知。
  1. 0:008> x coreclr!*settings*
  2. 00007ff9`e7f82e90 coreclr!WKS::gc_heap::settings = class WKS::gc_mechanisms
  3. 0:008> dt coreclr!WKS::gc_heap::settings 00007ff9`e7f82e90
  4.    +0x000 gc_index         : 0xb3
  5.    +0x008 condemned_generation : 0n2
  6.    +0x00c promotion        : 0n1
  7.    +0x010 compaction       : 0n0
  8.    +0x014 loh_compaction   : 0n0
  9.    +0x018 heap_expansion   : 0n0
  10.    +0x01c concurrent       : 1
  11.    +0x020 demotion         : 0n0
  12.    +0x024 card_bundles     : 0n1
  13.    +0x028 gen0_reduction_count : 0n0
  14.    +0x02c should_lock_elevation : 0n0
  15.    +0x030 elevation_locked_count : 0n0
  16.    +0x034 elevation_reduced : 0n0
  17.    +0x038 minimal_gc       : 0n0
  18.    +0x03c reason           : 0 ( reason_alloc_soh )
  19.    +0x040 pause_mode       : 1 ( pause_interactive )
  20.    +0x044 found_finalizers : 0n1
  21.    +0x048 background_p     : 0n0
  22.    +0x04c b_state          : 0 ( bgc_not_in_process )
  23.    +0x050 allocations_allowed : 0n1
  24.    +0x054 stress_induced   : 0n0
  25.    +0x058 entry_memory_load : 0x49
  26.    +0x060 entry_available_physical_mem : 0x00000001`0a50d000
  27.    +0x068 exit_memory_load : 0
复制代码
从 condemned_generation=2 可知当前触发的是 2 代GC,原因是代满了 reason           : 0 ( reason_alloc_soh ) 。
四:总结

看的再多还不如实操一遍,如果觉得手工编译 coreclr 源码麻烦,可以考虑下 windbg,好了,本篇就聊这么多,希望对你有帮助。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

滴水恩情

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表