GC 不实时回收内存,仅在特定条件触发,各代回收成本差异大;Gen 0 回收快(毫秒级)、代价低,90% 临时对象在此消亡;Gen 2 回收需扫描全堆及 LOH,可能卡顿数百毫秒,易致超时。

GC 不会实时回收内存,也不会在你“用完对象”那一行就立刻释放——它只在特定条件满足时触发,且不同代的回收成本差异极大。盲目调用 GC.Collect() 不仅无效,还可能拖慢程序。
Gen 0/1/2 回收行为差异直接影响性能
分代不是概念噱头,而是真实影响暂停时间和吞吐量的底层机制。Gen 0 回收快(毫秒级)、代价低,90% 的临时对象在此消亡;Gen 2 回收则需扫描整个托管堆+LOH,一次可能卡住几百毫秒,尤其在服务端场景下极易引发超时。
- 新分配的小对象(Gen 0,比如局部
List<double></double>、临时数组 -
Gen 0存活对象晋升到Gen 1,Gen 1存活再进Gen 2;大对象(如new byte[100000])直接进 LOH,归属Gen 2 -
Gen 2回收由系统内存压力或显式调用触发,不按分配节奏走——这意味着你无法靠“多建几个对象”来预测它何时发生
大对象堆(LOH)是隐性性能杀手
一旦对象 ≥ 85,000 字节(约 10,000 个 double),就会跳过 Gen 0/1 直接落进 LOH。LOH 默认不压缩,碎片化后即使总空闲空间充足,也可能因找不到连续块而被迫触发 Gen 2 回收。
-
List<double></double>容量增长时若突破阈值(例如从 10,000 扩到 15,000),底层数组重分配会生成新大对象,旧数组立即变垃圾,但 LOH 不整理,碎片累积 - .NET 5+ 支持启用 LOH 压缩:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;,但需手动触发且只生效一次 - 避免反复扩容:预估大小初始化
new List<double>(estimatedCount)</double>,比默认构造后 Add 更省 GC
什么时候该干预 GC,什么时候不该
绝大多数场景下,让 GC 自行调度是最优解。人工干预只在极少数明确可量化收益的节点才有意义,且必须配合监控验证。
- ✅ 可考虑
GC.Collect(0):长周期循环内反复创建大量短期对象(如图像帧处理),且已确认 Gen 0 回收滞后导致内存陡升 - ❌ 禁止
GC.Collect()或GC.Collect(2):UI 响应逻辑、HTTP 请求处理、构造函数中——这等于主动引入 STW 暂停,破坏响应性 - ⚠️
GC.SuppressFinalize()仅用于实现IDisposable的类型中,在Dispose()内调用,防止 Finalizer 再次排队——和内存回收无直接关系
真正关键的优化点不在“怎么催 GC”,而在“怎么少给 GC 留垃圾”:控制对象生命周期、复用缓冲区(如 ArrayPool<double>.Shared.Rent()</double>)、避免闭包捕获大对象、及时清空集合引用。这些动作的效果远超任何一次 GC.Collect() 调用。


















