在 CoreCLR 的托管进程中,内存被清晰地划分为两类: 原生堆(Native Heaps)托管堆(Managed Heaps) 。原生堆由CLR自身或通过P/Invoke调用的非托管代码使用,其生命周期由开发者显式控制。而托管堆是程序主体——所有托管对象——的栖身之所。这个堆由垃圾回收器(GC)全权管理,开发者无需关心对象何时释放,GC负责在恰当的时机回收不再使用的内存。

GC何时启动?有三种情形会触发一次回收。第一,操作系统发出物理内存吃紧的信号。第二,托管堆中已分配对象所占用的内存超过一个动态调整的阈值。这个阈值并非硬编码,CLR会持续观察各代对象的生存状况并实时微调。第三,代码显式调用GC.Collect()

GC工作的核心是可达性分析。它从一组“根”(Roots)出发——根包括静态字段、局部变量、CPU寄存器中的对象引用等——顺着引用链遍历,凡是能被触及的对象即为可达,反之则为不可达。GC随后释放所有不可达对象占用的内存,并将可达对象通过内存复制函数移动到一起,使它们在堆上紧凑排列。这个移动并修正引用的过程称为压缩(Compaction),它的目的只有一个:消除内存碎片,保证后续大对象分配有足够的连续空间。

为了平衡回收效率与吞吐量,GC为托管堆上的对象设计了 代(Generations) 的划分:0代、1代和2代。代并非物理分区,而是逻辑上的分组,用以表达一个对象的“存活预期”。新分配的对象属于0代。每次回收后,幸存下来的对象会晋升到更高一代——0代晋升至1代,1代晋升至2代。代龄越高,意味着它经历过越多次回收的考验,被假定为更有可能长期存活。

大对象堆(LOH)在逻辑上被视为2代(有时也称作第3代),但它独立存储。LOH专门容纳尺寸超过85KB的对象(这个85KB是可以被设置的,不设置的情况下默认是 85KB)。GC对LOH执行回收和标记,但不进行压缩——移动大块内存的代价过高,CLR选择通过其他手段管理其空闲空间,以控制碎片化。

GC的触发策略是逐级递进的,而不是“一上来就全盘清理”。绝大多数回收仅限定在0代,因为0代通常能释放出大量短期对象占用的空间,成本最低。只有当0代回收后仍无法满足新对象的分配需求时,GC才会进一步回收1代;若依然不够,才会触发代价最高的2代全量回收(Full GC)。在这次全量回收中,0代幸存者升到1代,1代幸存者升到2代。

整个机制的背后,CLR持续做着一道动态平衡题:回收不能太频繁(浪费CPU),也不能太迟(占用过多内存)。GC通过观察每一代对象的 幸存率(Survival Rate) 来动态调整触发阈值。如果某一代幸存率偏高,说明当前回收频率过于激进,GC会适度调高该代的阈值,让堆容纳更多对象后再启动回收,从而在内存占用与回收开销之间找到最优的平衡点。

这就是CoreCLR GC的设计轮廓:一个基于分代、逐级触发、动态自适应的内存管理系统。

参考资料