JVM堆分代设计

4 阅读8分钟

JVM垃圾收集的分代理论:从假说到实践的深度剖析

JVM的垃圾收集(GC)是现代自动内存管理的巅峰之作,而分代理论是贯穿其设计的核心思想。它并非凭空产生,而是基于对程序运行行为的长期观察,并经过数十年演化,形成了从理论假说到具体实现、再到不断优化的完整体系。下面从深度和广度两个维度,系统梳理这一逻辑脉络。


一、理论基石:分代假说(Generational Hypotheses)

分代理论的起点是对程序中对象生命周期统计特征的洞察,这些洞察被总结为两条基本假说:

  1. 弱分代假说(Weak Generational Hypothesis)
    内容:绝大多数对象的生命周期极短,往往在创建后不久便不再被引用。
    实证:大量应用程序的堆转储分析表明,80%~98%的对象在年轻代中死亡。
    推论:应该优先且频繁地回收年轻代,因为这里集中了大部分垃圾,回收性价比最高。

  2. 强分代假说(Strong Generational Hypothesis)
    内容:对象存活时间越长,它未来被回收的概率越低。
    实证:经过几次垃圾收集仍然存活的对象,往往成为长期存活的“老年代”对象(如缓存、单例、上下文对象)。
    推论:应将长期存活的对象集中管理,避免它们在年轻代中反复复制,同时采用适合高存活率的算法。

这两条假说共同奠定了分代收集的逻辑基础:将堆划分为年轻代和老年代,对不同区域采取差异化策略,以最小化整体收集开销


二、分代堆设计:物理划分与逻辑角色

基于上述假说,JVM将堆划分为两个主要逻辑区域:

  • 年轻代(Young Generation)

    • Eden区:绝大多数对象出生地。
    • Survivor区(通常两个,From和To):用于存放每次Minor GC后存活的对象,通过复制算法实现年龄增长。
    • 设计目标:利用弱分代假说,快速回收大量短命对象,降低停顿。
  • 老年代(Old Generation)

    • 存放经过多次Minor GC仍然存活的对象,以及某些直接分配的大对象。
    • 设计目标:利用强分代假说,集中管理长生命周期对象,采用适合高存活率的算法,并减少复制开销。

这种划分并非随意,而是通过大量实验确定的典型比例(如年轻代占堆的1/3,Eden与Survivor比例为8:1),既保证了年轻代有足够空间容纳新生对象,又为Survivor预留了足够的缓冲区。


三、各代清理算法:根据存活率选择最佳策略

分代堆的核心优势在于,可以根据不同代的存活率特征,选择最合适的垃圾收集算法。

1. 年轻代:复制算法(Copying)

  • 原理:将内存分为两块(逻辑上为Eden和一块Survivor),每次只使用其中一块。GC时将存活对象复制到另一块空闲Survivor,然后一次性清除原块所有内存。
  • 为什么适合年轻代
    • 存活率低 → 只需复制少量对象,大部分内存瞬间清空。
    • 复制后内存紧凑 → 后续分配采用指针碰撞(Bump-the-Pointer),速度快。
    • 无需标记所有对象,只需标记存活对象,效率高。
  • 代价:浪费一块Survivor空间(空闲),但年轻代空间不大,且复制算法的时间效率远高于空间浪费。

2. 老年代:标记-清除(Mark-Sweep) 或 标记-整理(Mark-Compact)

  • 为什么不能用复制算法
    • 老年代存活率高 → 复制大量存活对象开销极大,且需要额外空间(复制需要保留一块空闲区),空间利用率低。
  • 标记-清除(如CMS)
    • 原理:先标记存活对象,再统一清除未标记对象。
    • 优点:无需移动对象,可并发执行,停顿短(适合低延迟)。
    • 缺点:产生内存碎片,可能导致后续分配失败。
  • 标记-整理(如Parallel Old)
    • 原理:标记存活对象后,将它们向一端移动,然后清理边界外的内存。
    • 优点:消除碎片,内存紧凑,分配快速。
    • 缺点:需移动对象,全程暂停,停顿长(但总GC时间短,适合高吞吐量)。

算法选择的本质权衡:低延迟与高吞吐量不可兼得,分代设计允许在不同代之间采用不同算法,但老年代的算法选择直接影响应用的整体性能特征。


四、实现中的关键机制:解决分代带来的挑战

分代设计虽然高效,但在实现中引入了一系列问题,每个问题都催生了精巧的解决方案。

1. 跨代引用(Cross-Generation Reference)

  • 问题:Minor GC需要知道老年代对象是否引用了年轻代对象,否则可能误回收存活对象。如果每次Minor GC都扫描整个老年代,将丧失分代优势。
  • 解决方案
    • 记忆集(Remembered Set):一种抽象数据结构,记录从非收集区域(老年代)指向收集区域(年轻代)的引用。
    • 卡表(Card Table):HotSpot的具体实现,将老年代划分为512字节的卡,用字节数组标记每张卡是否有跨代引用。
    • 写屏障(Write Barrier):在每次引用赋值时插入额外代码,若赋值导致老年代引用年轻代,则将对应卡标记为“脏卡”。
    • Minor GC时:只扫描脏卡覆盖的老年代区域,找出跨代引用作为额外GC Roots,大幅缩小扫描范围。

2. 对象晋升与空间分配担保

  • 问题:Minor GC后存活对象需从Survivor复制或晋升到老年代,若老年代空间不足,晋升失败可能触发Full GC。
  • 解决方案
    • 年龄计数:对象每在Survivor中幸存一次,年龄加1,存储在对象头中(最大15)。
    • 动态年龄判定:若某年龄对象总大小超过Survivor空间的一半(TargetSurvivorRatio),年龄大于等于该值的对象直接晋升。
    • 空间分配担保:Minor GC前检查老年代最大连续空间是否大于历次晋升到老年代对象的平均大小,若大于则进行Minor GC,否则直接进行Full GC(避免多次失败)。

3. 老年代碎片与并发失败

  • 问题:标记-清除算法(如CMS)导致碎片,可能无法分配大对象;并发收集期间若老年代被填满,会导致“并发模式失败”,退化为Full GC。
  • 解决方案
    • CMS提供压缩选项-XX:CMSFullGCsBeforeCompaction,设置多少次CMS后进行压缩式Full GC。
    • G1的Region设计:将堆划分为多个独立Region,通过复制算法在回收老年代Region时进行局部压缩,避免全局碎片。
    • 调整触发阈值:降低CMS启动阈值(CMSInitiatingOccupancyFraction),为浮动垃圾预留空间。

4. 不同GC类型的协调

  • 问题:分代产生了Minor GC、Major GC、Full GC三种类型,需要明确定义和触发机制,避免混乱。
  • 解决方案
    • 明确触发条件(如前文所述)。
    • GC日志区分:通过日志前缀和阶段标识帮助定位问题。
    • 收集器自身协调:如G1的Mixed GC逐步回收老年代,避免Full GC;CMS在并发失败时退化为Serial Old。

五、分代思想在不同垃圾收集器中的演进

分代理论不仅体现在经典收集器中,也在现代收集器中以更灵活的方式延续。

收集器分代实现特点算法组合核心目标
Serial / Parallel传统分代堆,年轻代复制,老年代标记-整理复制 + 标记-整理简单、高吞吐量
CMS分代堆,老年代用标记-清除并发收集复制 + 标记-清除低延迟
G1逻辑分代,物理Region;每个Region可扮演不同代复制(年轻代)+ 复制+标记-清除(老年代Mixed GC)可预测停顿,兼顾吞吐量
ZGC / Shenandoah逻辑上仍有分代概念(如ZGC的分代模式),但实现基于染色指针、读屏障等并发标记-整理极低延迟,超大堆

G1的创新:不再物理连续,但保留分代逻辑,通过RSet(记忆集的细化)记录跨Region引用,在Mixed GC中优先回收价值高的老年代Region,实现了停顿可控的增量压缩。

ZGC的分代尝试:最新版本的ZGC已引入分代模式,以处理年轻代对象的快速回收,同时保持并发,说明分代思想即使在最先进的收集器中仍具生命力。