让 .NET GC 行为可观察:我在构建 GCExperiment 时的收获
Source: Dev.to
为什么进行实验?
文档告诉你 GC 做了什么。运行代码则告诉你 何时 以及 为什么。在尝试推理真实系统中的分配压力时,这两者的区别非常重要。
该项目名为 GCExperiment —— 四个相互独立的实验,每个实验针对一种 GC 机制,全部配备真实的快照进行仪表化。
基于 .NET 10 / C# 14,仅支持 x64。x86 上的头部大小和对齐方式不同,因此需要 64 位才能获得真实的 LOH 行为。
实验 1 — LOH 放置
我学到的最令人惊讶的事情是:这并不是一个简单的 85,000 字节阈值。
GC 测量的是 完整对象成本——包括头部、有效负载以及对齐。下面是 byte[84_999] 的实际计算过程:
// Full size calculation
24 // array header
+ 84_999 // payload
= 85_023 → aligned to 85_024 → LOH threshold hit因此 byte[84_999] 会进入大对象堆(Large Object Heap)——这不是因为负载超过了 85 KB,而是因为对齐后的完整对象超过了阈值。
实验结果显示
- 不同大小的数组实际落在哪个堆(SOH 与 LOH)
- 如何使用
GCInfo.EstimateSizeByFactory测量完整对象大小 - 如何在需要时强制进行 LOH 紧缩:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(2, GCCollectionMode.Forced, blocking: true);实验 2 — 代提升
对象在 Gen0 中并不会永远存活。存活下来的对象会被提升——本实验实时展示了这一过程。
| 代数 | 含义 |
|---|---|
0 | 最近分配的,短暂的 |
1 | 已经经历过一次 Gen0 垃圾回收仍然存活 |
2 | 长期存活的,或位于 LOH(LOH 对象会立即从 Gen2 开始) |
有一点我没预料到:GC.KeepAlive 在观察提升时真的会改变结果。如果不使用它,JIT 可能会优化掉你的引用,导致对象在你检查其代数之前就被回收。
实验 3 — 分配压力
并排比较两种模式:
- 短命小对象 — 分配、丢弃、重复 → 强化 Gen0
- 长命大对象 — 产生 Gen2/LOH 压力
输出在实验运行时打印实时的垃圾回收计数。更改样本大小并观察 Gen0 与 Gen2 计数的差异,有助于建立直观认识。
实验 4 — LOH 碎片化
如果交替进行大对象分配——分配 A,分配 B,释放 A,尝试分配 C——A 留下的空洞可能容不下 C。默认情况下 LOH 不会进行压缩,因此这些空洞会累积。
[ A ][ B ][ hole ][ B ][ hole ]
↑
C doesn't fit here这就是 LOH 压缩和缓冲区池(ArrayPool)存在的原因。该实验通过分配失败和 GC 快照比较,使碎片化可见。
诊断与辅助工具
该仓库包含两个仪器辅助工具:
GCMonitor(位于 GCMastery.Diagnostics)
GCMonitor.PrintSnapshot("after allocation");
GCMonitor.PrintObjectGeneration(myObj, nameof(myObj));
GCMonitor.Reset("clean baseline");GCInfo(位于 GCExperiment)
var state = GCInfo.GetCurrentState(forceFullCollectionFirst: true);
var gen = GCInfo.GetGeneration(myObj);
var size = GCInfo.EstimateSizeByFactory(() => new byte[84_999], sampleCount: 1000);运行所有
GenerationExperiment.Run();
LOHExperiment.Run();
FragmentationExperiment.Run();
PressureExperiment.Run();在测量任何东西之前的一个提示 — 始终从干净的状态开始
GC.Collect(2, GCCollectionMode.Forced, blocking: true);
GC.WaitForPendingFinalizers();
GC.Collect(2, GCCollectionMode.Forced, blocking: true);否则,之前运行留下的状态会污染你的结果。
我仍在探索的内容
- Server GC 与 Workstation GC 行为差异
GCSettings.LatencyMode如何影响晋升时机- 更好的可视化碎片随时间变化的方法(目前仅限控制台)
如果你在这些方面有经验并发现了问题——或者有值得加入的实验想法——欢迎在评论中提供反馈。
仓库
👉
需要 .NET 10 · C# 14 · x64