本节摘要:CLR 的垃圾回收器基于"对象越年轻越容易死"的分代假说,把堆划成三代加一个大对象堆。理解各代的回收行为,才能读懂内存性能工具的输出;减少分配(Span、结构体、池化)是比调优 GC 参数更根本的优化。本节收束全书的机制主线。
GC 的核心困境:标记存活对象必须扫堆,堆越大停顿越长。CLR 的应对是分代——赌"大多数对象朝生暮死"。经验上这个赌注极准:临时对象(查询结果、拼接字符串、状态机)绝大多数在诞生后很快无引用。于是堆被划成三块:
第 0 代:新对象的育婴房,空间很小(通常几 MB)。0 代满了触发 0 代回收——只扫这一小块,毫秒内清走一大批短命对象,幸存者晋升 1 代。绝大多数对象死在这里,一次都没拖累过大扫描。
第 1 代:0 代幸存者的缓冲带,同样不大。1 代回收扫 0 加 1 两块,仍属轻量。经历两次回收还活着的对象晋升 2 代——能扛过两轮清理的对象,大概率是长命的(缓存、单例、长连接会话)。
第 2 代:老年代,与 1 代加起来构成主堆。2 代回收是全堆扫描,也是停顿的大头,健康程序里应当极少发生。另外有个例外通道:大对象堆 LOH,约 85KB 以上的对象直接分配在 LOH,不经过 0 代。LOH 默认不压缩整理,反复分配大数组会造出内存碎片——总空闲够但连不成片,最终触发本可避免的 2 代回收。

动手验证一次,把机制变成亲眼看过的东西:
var timer = new System.Timers.Timer(1000); // 长寿对象 timer.Start(); for (int i = 0; i < 5; i++) { var temp = new byte[1024 * 100]; // 短命对象 GC.Collect(); Console.WriteLine($"临时对象在第 {GC.GetGeneration(temp)} 代"); } GC.Collect(); GC.Collect(); Console.WriteLine($"计时器在第 {GC.GetGeneration(timer)} 代"); // 2
临时对象的代数逐轮上涨(0、1、2)——它每次都在回收中幸存所以晋升;计时器在强制两轮回收后到达 2 代。这个十几行的小实验胜过千字描述。
内存问题的标准排查顺序:先看 GC.CollectionCount 三代曲线与进程内存曲线定性(2 代爬升快是长寿对象泄漏或 LOH 碎片;0 代疯狂是分配过量);再用分配时间线工具(dotMemory、VS 诊断或事件计数器)抓分配热点,看哪个类型在哪条调用路径上大量产生;最后回到代码。三个高频诊断结论可以直接备着:内存只涨不降多半不是"GC 不干活",而是有静态集合、事件订阅(2.6 节的退订阅坑)、缓存无上限把对象钉住了——GC 只回收无引用对象,被引用的到死都是存活的;请求处理越到后面越慢常见于 LOH 碎片(每请求拼一个大数组);0 代回收次数天文数字就是纯粹的分配过量,进入下一小节。
手法一:Span 切视图。Span<T> 与 ReadOnlySpan<T> 是"指向连续内存的窗口":不复制数据,只记录起点与长度。第一章说过 Substring 必然分配新串,Span 版的切片零分配:
ReadOnlySpan<char> text = "user=alice;role=admin"; int eq = text.IndexOf('='); ReadOnlySpan<char> user = text.Slice(5, eq - 5); // 无任何新分配
解析协议、切分报文这类高频路径,Span 能把分配砍掉九成。它的纪律也来自本质(是栈上的窗口不是对象):不能存进字段跨 await 携带、不能装箱。配套的 stackalloc 在栈上开小缓冲、ArrayPool 租还数组,组成零分配三件套。
手法二:结构体与值语义。第一章的账这里结清:一个含 int 的类对象占 24 字节且是 GC 追踪对象,struct 只占 4 字节且不劳 GC。高频小数据(点、金额、id 对)改 struct,分配与回收压力同时消失。1.3 节的门槛(小、不可变、值语义)不变,外加一条:避免无意识装箱。
手法三:池化复用。ObjectPool 与 ArrayPool<T>.Shared 的思路是"贵的东西用完还回来":大缓冲区、序列化用的 StringWriter、数据库上下文,租用后归还,下个请求免掉分配。池化的成本是把生命周期管理从 GC 手里拿回到你自己手上——忘了还就是逻辑泄漏。适合"形状固定、创建昂贵、高频进出"的对象。
💡 收束全书的直觉:你在第一章看到的对象布局、装箱价格、字符串分配,在第三章看到的集合内部数组,在 4.1 节看到的状态机分配,最终都汇进这一节的分代模型——.NET 性能工程的核心只有一句话:管好你的分配,GC 就没有存在感。