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 收集器:采用分代策略,年轻代使用复制算法,老年代使用标记-整理算法:
✅ 吞吐量高
❌ 存在较长的 STW 暂停CMS(Concurrent Mark Sweep)收集器:Java 1.4 引入,是并发、低暂停的分代收集器,年轻代使用复制算法,老年代使用标记-清除算法:
✅ 并发执行,暂停时间短
❌ CPU 占用高、暂停时间仍不可控、不适用于大于 4GB 的堆G1(Garbage First)收集器:Java 7 引入,用于替代 CMS,是一种并发、增量整理、低暂停的分代收集器:
✅ 基于 Region 的结构,暂停时间更可预测
❌ 仍存在暂停,且调优复杂
随着对更低延迟的追求,JVM 引入了多个实验性收集器,如 ZGC、Epsilon 和本文重点介绍的 Shenandoah。
3. Shenandoah 垃圾收集器
Shenandoah 是一个从 Java 12 开始引入的实验性收集器,定位为延迟优先型 GC。它通过将大部分 GC 工作与用户程序并发执行,来大幅降低暂停时间。
✅ 无论堆大小如何,都能提供一致的低暂停时间
❌ 实验性,需手动启用
3.1. 堆结构
Shenandoah 与 G1 类似,采用 Region 化堆结构,将堆划分为多个大小相等的区域:
⚠️ 与 G1 不同的是,Shenandoah 不区分代(年轻代/老年代),因此每次 GC 都需遍历整个堆,标记所有存活对象。
3.2. 对象布局
Java 中的对象不仅包含数据字段,还包括元数据,如对象头(Header)和标记字(Mark Word):
Shenandoah 在对象布局中新增了一个字段:Brooks Pointer(间接指针),用于支持并发对象移动,避免更新所有引用。
3.3. 屏障机制(Barriers)
并发 GC 的核心难点在于:GC 与用户程序同时运行时,如何保证堆的正确性。Shenandoah 使用多种屏障机制来拦截堆访问:
主要包括:
- ✅ 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):
为了支持并发标记,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 版本:
📌 大部分工作并发执行,仅在 init-update-refs
和 final-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 等为低延迟场景提供了更多选择。
📌 不建议将它们视为高吞吐收集器的直接替代品,需根据实际业务场景评估。