这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 Shenandoah 是 OpenJDK 中另一个并发整理型低停顿垃圾收集器。它最早由 Red Hat 主导开发,目标是减少 GC 停顿对大堆服务的影响。与 ZGC 类似,它也在标记、整理、引用修正上大量使用并发执行;但实现细节不同,演进路径也不同。
Shenandoah 在不同发行版和 JDK 版本中的可用性存在差异。某些商业发行版长期支持,某些版本可能默认关闭或不包含。使用前应以当前运行时和发行版文档为准。
19.1 设计目标
Shenandoah 的核心目标是:
- 大部分 GC 工作与应用并发执行;
- 支持大堆;
- 停顿时间不依赖堆大小;
- 尽量减少压缩带来的全局停顿。
应用线程 GC 线程
| |
| <-- 极短 STW --> |
| ------ 并发 ------> | 标记
| ------ 并发 ------> | 整理
| ------ 并发 ------> | 修正引用
| |
它同样不是零停顿。根扫描、部分同步点仍会短暂停止应用线程。
19.2 转发指针
早期 Shenandoah 使用 Brooks Forwarding Pointer,为每个对象额外维护一个转发指针。
对象头
+-----------------------+-------------------+
| 原本对象头和字段 | forwarding pointer |
+-----------------------+-------------------+
|
v
新对象地址
对象被搬迁后,旧对象的转发指针指向新对象。应用线程访问旧地址时,通过转发指针找到新地址。
这种方式的优点是不依赖 64 位地址中的空闲位;缺点是对象布局变大,访问路径增加。
19.3 加载引用屏障
新版 Shenandoah 引入加载引用屏障,减少屏障次数,只在读取引用时执行必要处理。
简化流程:
读取一个对象引用
-> 引用是否需要处理?
-> 否:直接使用
-> 是:
1. 判断对象是否已被搬迁
2. 需要时读取新地址
3. 更新引用
4. 返回正确对象
这与 ZGC 的读屏障思想相近,但实现和屏障插入位置不同。两者都把“修正引用”的一部分工作分散到应用线程执行。
19.4 GC 周期
Shenandoah 的周期可以简化为:
1. 初始标记 STW,扫描部分根
2. 并发标记 与应用并发标记对象图
3. 最终标记 STW,完成标记并准备整理
4. 并发整理 选择 Region 并搬迁存活对象
5. 并发清理 释放空 Region
不同模式的并发程度不同。为了应对紧急内存压力,Shenandoah 也存在降级模式,允许牺牲停顿目标换取回收速度。
| 模式 | 特点 |
|---|---|
| 正常并发模式 | 停顿低,吞吐有屏障成本 |
| 降级模式 | 回收更激进,停顿可能变长 |
| Full GC | 兜底,通常意味着压力过高或配置不当 |
19.5 启用方式
如果当前 JVM 支持 Shenandoah:
java -XX:+UseShenandoahGC -Xms16g -Xmx16g -jar app.jar
查看是否支持:
java -XX:+UseShenandoahGC -version
如果输出 Unrecognized VM option,说明当前运行时没有启用或没有包含该收集器。
19.6 与 ZGC 对比
| 维度 | Shenandoah | ZGC |
|---|---|---|
| 引用处理 | 加载引用屏障,早期使用转发指针 | 着色指针 + 读屏障 |
| 出身 | OpenJDK 社区,Red Hat 主导早期开发 | Oracle 主导进入 HotSpot |
| 目标 | 低停顿、并发整理 | 低停顿、大堆 |
| 分代 | 后续版本持续演进 | JDK 21 引入分代 |
| 可用性 | 与发行版关系更大 | HotSpot 常见发行版通常可用 |
两者不是简单的新旧关系,而是同一目标下的不同工程取舍。实际选择必须看 JDK 发行版、版本、业务负载和压测结果。
19.7 生产观察
启用日志:
java -XX:+UseShenandoahGC \
-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=10,filesize=50m \
-jar app.jar
关注:
Pause Init Mark
Concurrent Marking
Pause Final Mark
Concurrent Evacuation
Concurrent Cleanup
Allocation Stall
Degenerated GC
Full GC
健康信号:
- 少量 Degenerated GC;
- 没有 Full GC;
- 停顿分布稳定;
- Live 数据不持续增长;
- CPU 有余量支撑并发阶段。
19.8 常见问题
| 现象 | 可能原因 | 处理 |
|---|---|---|
| Degenerated GC 频繁 | 堆余量不足、分配过快 | 增大堆或降低分配 |
| Full GC | 回收速度跟不上、配置问题 | 检查堆、对象和日志 |
| 吞吐下降 | 屏障成本 | 对比 G1 或 ZGC |
| CPU 抖动 | 并发 GC 线程争抢 | 检查 CPU limit 和线程数 |
| 容器 OOM | 堆外内存超限 | 统计 Native、线程、直接内存 |
19.9 使用建议
- 先确认 JDK 发行版明确支持 Shenandoah;
- 生产环境固定
-Xms和-Xmx; - 给堆留出足够余量,避免长期高占用;
- 对比测试 G1、ZGC、Shenandoah 的延迟分布;
- 关注发行版升级日志,因为 Shenandoah 的屏障和调度策略变化较快;
- 不建议只因为“低停顿”就在内存紧张的容器中盲目启用。
本章小结
Shenandoah 通过转发指针、加载引用屏障和并发整理,把对象搬迁与引用修正大部分移出全局停顿。它与 ZGC 解决的是类似问题,但实现路径和版本可用性不同。生产使用前要先确认运行时支持,再通过日志确认是否出现 Degenerated GC、Full GC 和 Allocation Stall,并用同流量压测比较延迟与资源成本。
思考题
- 转发指针如何帮助并发整理?
- 加载引用屏障和早期转发指针方案有什么取舍?
- Degenerated GC 频繁出现说明什么?
- Shenandoah 与 ZGC 的可用性判断有什么不同?
- 为什么不能只看官方停顿目标选择收集器?