垃圾收集的核心挑战

垃圾收集器的设计始终面临三个核心指标的权衡:吞吐量、延迟和内存占用。这三个指标构成了一个不可能三角,优化其中一个指标往往需要牺牲其他指标。

吞吐量指单位时间内完成的工作量,通常用应用程序运行时间占总时间的比例来衡量。对于批处理任务、科学计算等场景,高吞吐量是首要目标。

延迟指垃圾收集造成的应用停顿时间。对于交互式应用、金融交易系统等对响应时间敏感的场景,低延迟至关重要。延迟通常关注最大停顿时间和停顿时间分布。

内存占用指垃圾收集器为了完成收集工作所需的额外内存空间。内存受限的环境下,收集器自身的内存开销成为关键约束。

传统垃圾收集器在三者之间做出明确取舍:Serial 和 Parallel GC 追求高吞吐量,但停顿时间较长;CMS GC 降低停顿时间,但牺牲吞吐量并占用更多内存。现代收集器试图在三者之间找到更优的平衡点。

G1(Garbage First)收集器

G1 收集器在 JDK 7u4 版本正式推出,是 JDK 9 之后的默认垃圾收集器。G1 的核心思想是打破传统分代收集的物理隔离,将堆内存划分为多个大小相等的 Region。

Region 化堆内存

G1 将堆内存划分为多个大小相等的独立区域,每个 Region 可以独立作为 Eden 区、Survivor 区或 Old 区。Region 的大小通常在 1MB 到 32MB 之间,数量可达数千个。这种设计带来两个优势:可预测的停顿时间和灵活的内存分配。

G1 通过跟踪每个 Region 的垃圾堆积效率,优先回收垃圾最多的 Region,这也是 “Garbage First” 名称的由来。这种策略确保在有限的时间内回收最多的内存空间。

混合收集和并发标记

G1 的收集过程分为 Young GC、Mixed GC 和 Full GC。Young GC 仅回收新生代 Region,Mixed GC 同时回收新生代和老年代 Region。Full GC 退化为单线程收集,应尽量避免。

G1 采用并发标记算法,标记过程分为三个阶段:初始标记、并发标记和最终标记。初始标记需要 STW(Stop-The-World),但只扫描 GC Roots 直接关联的对象;并发标记与应用程序并发执行;最终标记处理并发阶段遗留的引用变更。

停顿时间目标

G1 允许用户通过 -XX:MaxGCPauseMillis 参数设置期望的最大停顿时间,默认 200ms。G1 会根据历史数据预测每次收集能回收的 Region 数量,在停顿时间内尽可能多地回收垃圾。

如果实际停顿时间频繁超过目标,G1 会调整策略:减少每次回收的 Region 数量或提高收集频率。这种自适应机制使 G1 能够在动态变化的负载下维持相对稳定的停顿时间。

卡表和 RSet

跨代引用是分代收集的难点。G1 使用卡表和 RSet(Remembered Set)来高效处理跨代引用。卡表记录老年代中哪些卡页包含指向新生代的引用,每个卡页大小为 512 字节。

RSet 是每个 Region 独立维护的数据结构,记录其他 Region 中指向本 Region 的引用。RSet 的实现基于哈希表,存储引用来源 Region 的卡页索引。这种设计使 G1 能够避免全堆扫描,只扫描 RSet 记录的 Region。

ZGC 收集器

ZGC 在 JDK 11 中作为实验特性引入,JDK 15 正式发布。ZGC 的设计目标是实现亚毫秒级停顿,并支持 TB 级堆内存,同时保持合理的吞吐量。

着色指针

ZGC 的核心创新是着色指针技术。ZGC 在 64 位指针中保留 42 位用于寻址,支持的最大堆内存为 16TB。剩余的 22 位用于存储元数据:4 位用于颜色标记(Finalizable、Remapped、Marked0、Marked1),18 位保留。

着色指针将对象的状态直接编码到指针中,无需额外的数据结构。当对象需要移动时,ZGC 修改指针的值而不是对象本身,这种设计消除了对象移动时的写屏障开销。

读屏障

ZGC 在每次读取对象引用时插入读屏障。读屏障检查指针的颜色标记,如果发现指针指向的对象正在被移动,则触发转发操作,将指针更新到新位置。读屏障的开销极小,通常只需要几条 CPU 指令。

读屏障的实现依赖于 CPU 的 load barriers 指令(x86 架构的 lfence 指令)。ZGC 通过动态编译技术,在热点代码路径中内联读屏障,进一步降低开销。

并发整理

ZGC 的最大特点是全并发的整理过程。传统收集器的对象整理需要 STW,而 ZGC 在应用程序运行的同时移动对象。当对象需要移动时,ZGC 在原位置保留转发指针,所有通过读屏障访问该对象的请求都会被重定向到新位置。

ZGC 使用多重映射技术,将对象的多个视图映射到同一个物理内存区域。这种设计使 ZGC 能够在不修改对象引用的情况下完成对象移动,极大降低了 STW 时间。

亚毫秒级停顿和 TB 级堆

ZGC 的停顿时间通常在 1ms 以内,与堆大小无关。即使堆内存达到 TB 级别,ZGC 仍能维持亚毫秒级的停顿。这种特性使 ZGC 适用于对延迟极其敏感的大内存应用。

ZGC 的吞吐量略低于 Parallel GC,但显著优于 CMS 和 G1。根据官方基准测试,ZGC 的吞吐量约为 Parallel GC 的 70% 到 90%。

Shenandoah 收集器

Shenandoah 收集器由 Red Hat 开发,在 JDK 12 中作为实验特性引入。Shenandoah 的设计目标与 ZGC 类似,但采用不同的技术实现路径。

Brooks 转发指针

Shenandoah 使用 Brooks 转发指针实现并发整理。每个对象头中额外存储一个转发指针字段,初始指向对象自身。当对象需要移动时,Shenandoah 更新转发指针指向新位置,同时保留原对象的内容。

通过转发指针,Shenandoah 能够在不修改对象引用的情况下完成对象移动。这种设计避免了 ZGC 需要的 CPU 特定指令,使 Shenandoah 具有更好的平台移植性。

并发整理

Shenandoah 的整理过程包括并发标记、并发整理和并发更新引用三个阶段。并发标记阶段识别存活对象;并发整理阶段将存活对象移动到新的位置;并发更新引用阶段更新所有指向旧位置的引用。

Shenandoah 使用 Brooks 指针确保在整理过程中任何对旧对象的引用都能正确转发到新对象。转发指针的开销主要来自额外的间接访问,但现代 CPU 的分支预测和缓存机制能够有效降低这种开销。

与 ZGC 的异同

Shenandoah 和 ZGC 的设计目标高度一致,但实现路径不同。主要差异包括:Shenandoah 使用 Brooks 指针,ZGC 使用着色指针;Shenandoah 不依赖于 CPU 特定指令,ZGC 需要 load barriers 支持;Shenandoah 的对象头开销略大(增加转发指针字段),ZGC 的指针编码更复杂。

在性能方面,两家收集器的停顿时间都达到亚毫秒级别,吞吐量接近。Shenandoah 在某些场景下的延迟表现略优于 ZGC,但 ZGC 的平台优化更成熟。选择哪种收集器通常取决于具体的硬件环境和应用特性。

对比表格

特性 G1 ZGC Shenandoah
最大停顿时间 200ms(可配置) <1ms <1ms
吞吐量 中等 较高 较高
最大堆内存 32GB 16TB 16TB
JDK 版本 7u4+ 11+(15 正式) 12+
并发整理 不支持 支持 支持
适用场景 通用场景 低延迟大内存 低延迟大内存
CPU 要求 x86/ARM load barriers 无特定要求
内存开销 较低(RSet) 较低(着色指针) 较高(转发指针)

选型指导

选择 G1 的场景

G1 适用于大多数通用场景,特别是堆内存不超过 32GB、对停顿时间有适度要求的应用。如果应用运行在 JDK 8 或 JDK 9 环境,G1 是最成熟的选择。G1 的调优参数丰富,适合需要精细控制收集行为的场景。

选择 ZGC 的场景

ZGC 适用于对延迟极其敏感、堆内存较大的应用。如果应用运行在支持 load barriers 的 CPU 架构上(x86、ARM),并且使用 JDK 11 或更高版本,ZGC 能够提供最优的延迟表现。典型场景包括金融交易系统、实时广告投放系统等。

选择 Shenandoah 的场景

Shenandoah 适用于需要低延迟但硬件环境不支持 load barriers 的场景。如果应用运行在非 x86/ARM 架构上,或者使用 OpenJ9 等 JVM 实现,Shenandoah 是更好的选择。Shenandoah 也适用于需要更细粒度控制转发策略的场景。

与传统收集器的演进关系

Serial GC 是最早的垃圾收集器,采用单线程标记整理算法,适合单核 CPU 和小内存环境。Parallel GC 在 Serial GC 的基础上引入多线程,显著提高了吞吐量,成为 JDK 8 的默认收集器。

CMS GC 是第一个关注延迟的收集器,采用并发标记清除算法。CMS 降低了停顿时间,但存在内存碎片、CPU 占用高、浮动垃圾等问题,最终在 JDK 9 中被标记为过时。

G1、ZGC 和 Shenandoah 代表了垃圾收集技术的第三代演进。它们在降低延迟的同时,保持了合理的吞吐量和内存效率。这种演进反映了应用需求的变化:从追求吞吐量到追求低延迟,再到兼顾延迟、吞吐量和内存占用。

未来垃圾收集器的发展方向包括:更智能的自适应调优、更低的开销、更好的 NUMA 架构支持,以及与硬件特性的深度结合。AI 驱动的垃圾收集策略优化也可能成为下一个突破点。

总结

现代垃圾收集器的演进体现了 JVM 技术的持续进步。G1 在通用场景下提供了平衡的性能表现;ZGC 和 Shenandoah 在低延迟场景下实现了突破性的改进。选择合适的垃圾收集器需要综合考虑应用特性、硬件环境、JDK 版本等多个因素。

垃圾收集器的设计始终是在权衡中寻找最优解。理解各种收集器的原理和适用场景,能够帮助开发者和运维人员做出更明智的选择,从而提升应用的整体性能和稳定性。