1. 引言

本篇文章将带你了解 Java 内存管理的基本问题,以及为什么我们需要不断寻找更优的垃圾回收方式。我们将重点介绍 Java 中新引入的一个实验性垃圾收集器 —— Shenandoah,并与其他垃圾收集器进行比较。

2. 垃圾收集中的挑战

垃圾收集器(GC)是 JVM 等运行时系统提供的自动内存管理机制,负责程序运行时的内存分配与回收。常见的垃圾收集算法包括引用计数(Reference Counting)、标记-清除(Mark-Sweep)、标记-整理(Mark-Compact)以及复制算法(Copying)。

2.1. 垃圾收集器的设计考量

根据所采用的算法,垃圾收集器可以分为两类:

  • Stop-The-World(STW)模式:在用户程序暂停时执行回收,优点是吞吐量高,缺点是延迟大。
  • 并发模式:与用户程序并发执行,优点是延迟低,但可能牺牲部分吞吐量。

现代收集器通常采用混合策略,比如将堆划分为年轻代和老年代,年轻代使用 STW 回收,老年代采用并发或增量回收来减少暂停时间。

然而,理想中的垃圾收集器应当具备低延迟、高吞吐量,并在不同堆大小下保持可预测的行为。这正是 Java GC 持续演进的动力所在。

2.2. Java 中现有的垃圾收集器

一些传统的垃圾收集器包括:

  • Serial 和 Parallel 收集器:采用分代策略,年轻代使用复制算法,老年代使用标记-整理算法:

    Garbage Collector Serial Parallel 1

    ✅ 吞吐量高
    ❌ 存在较长的 STW 暂停

  • CMS(Concurrent Mark Sweep)收集器:Java 1.4 引入,是并发、低暂停的分代收集器,年轻代使用复制算法,老年代使用标记-清除算法:

    Garbage Collector CMS 1

    ✅ 并发执行,暂停时间短
    ❌ CPU 占用高、暂停时间仍不可控、不适用于大于 4GB 的堆

  • G1(Garbage First)收集器:Java 7 引入,用于替代 CMS,是一种并发、增量整理、低暂停的分代收集器:

    Garbage Collector G1-1

    ✅ 基于 Region 的结构,暂停时间更可预测
    ❌ 仍存在暂停,且调优复杂

随着对更低延迟的追求,JVM 引入了多个实验性收集器,如 ZGC、Epsilon 和本文重点介绍的 Shenandoah

3. Shenandoah 垃圾收集器

Shenandoah 是一个从 Java 12 开始引入的实验性收集器,定位为延迟优先型 GC。它通过将大部分 GC 工作与用户程序并发执行,来大幅降低暂停时间。

✅ 无论堆大小如何,都能提供一致的低暂停时间
❌ 实验性,需手动启用

3.1. 堆结构

Shenandoah 与 G1 类似,采用 Region 化堆结构,将堆划分为多个大小相等的区域:

Garbage Collector Shenandoah Heap Structure

⚠️ 与 G1 不同的是,Shenandoah 不区分代(年轻代/老年代),因此每次 GC 都需遍历整个堆,标记所有存活对象。

3.2. 对象布局

Java 中的对象不仅包含数据字段,还包括元数据,如对象头(Header)和标记字(Mark Word):

Garbage Collector Shenandoah Object Layout

Shenandoah 在对象布局中新增了一个字段:Brooks Pointer(间接指针),用于支持并发对象移动,避免更新所有引用。

3.3. 屏障机制(Barriers)

并发 GC 的核心难点在于:GC 与用户程序同时运行时,如何保证堆的正确性。Shenandoah 使用多种屏障机制来拦截堆访问:

Garbage Collector Barriers

主要包括:

  • ✅ SATB 屏障(用于标记阶段)
  • ✅ Read Barrier(读屏障)
  • ✅ Write Barrier(写屏障)

这些屏障虽然增加了开销,但确保了 GC 的正确性和并发性。

3.4. 模式、启发式策略与失败模式

  • 模式(Modes):定义 Shenandoah 的运行方式,如使用哪种屏障。默认为 normal/SATB

  • 启发式策略(Heuristics):决定何时触发 GC,如 adaptive(默认)、aggressive 等。

  • 失败模式(Failure Modes):当 GC 跟不上分配速度时,会触发降级策略,如:

    • ✅ Pacing(限速)
    • ⚠️ Degenerated GC(退化为 STW)
    • ❌ Full GC(最坏情况)

4. Shenandoah 收集阶段详解

Shenandoah 的 GC 周期主要分为三个阶段:标记(Mark)、疏散(Evacuate)和引用更新(Update References)。

4.1. 标记阶段(Marking)

标记阶段用于识别堆中所有存活对象。Shenandoah 使用三色标记法(White/Grey/Black):

Garbage Collector Shenandoah Marking 1

为了支持并发标记,Shenandoah 使用 SATB 算法,通过 SATB 屏障维护一致性视图。

📌 两个 STW 点:

  • init-mark:扫描根集
  • final-mark:处理队列、重扫根集、准备回收集

4.2. 清理与疏散阶段(Cleanup & Evacuation)

标记完成后,无存活对象的 Region 被清理。接着,将回收集中存活对象移动到其他 Region,以减少内存碎片。

Shenandoah 通过 CAS 操作更新 Brooks Pointer,将对象从 from-space 指向 to-space,实现并发移动。

📌 读写屏障用于维护 to-space 不变性,确保用户程序访问的是已迁移的副本。

4.3. 引用更新阶段(Reference Update)

该阶段遍历整个堆,将所有指向已移动对象的引用更新为 to-space 版本:

Garbage Collector Shenandoah Update Refs

📌 大部分工作并发执行,仅在 init-update-refsfinal-update-refs 时短暂 STW。

5. 与其他实验性收集器的比较

5.1. ZGC(Z Garbage Collector)

  • ✅ Java 11 引入,单代、低延迟、支持 TB 级堆
  • ✅ 使用 Load Barrier 和指针染色技术
  • ⚠️ 调优选项相对较少
  • 🔍 与 Shenandoah 目标相似,但 Shenandoah 更灵活

5.2. Epsilon GC

  • ✅ Java 11 引入,无 GC 功能,只分配不回收
  • ✅ 适用于内存使用可控、追求极致性能的场景
  • ❌ 堆满即崩溃
  • 🔍 与 Shenandoah 目标完全不同,主要用于基准测试

6. 总结

本文介绍了 Java 中垃圾收集的基本原理与挑战,并深入解析了 Shenandoah 收集器的设计思路与实现机制。与其他实验性收集器相比,Shenandoah 在低延迟与并发性方面表现突出,但作为实验性特性,仍需谨慎使用。

📌 G1 仍是默认收集器,但 Shenandoah、ZGC 等为低延迟场景提供了更多选择。
📌 不建议将它们视为高吞吐收集器的直接替代品,需根据实际业务场景评估。


原始标题:Experimental Garbage Collectors in the JVM