JVMNotes

第 19 章:Shenandoah

zjc 于 2026-01-19 发布

这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 Shenandoah 是 OpenJDK 中另一个并发整理型低停顿垃圾收集器。它最早由 Red Hat 主导开发,目标是减少 GC 停顿对大堆服务的影响。与 ZGC 类似,它也在标记、整理、引用修正上大量使用并发执行;但实现细节不同,演进路径也不同。

Shenandoah 在不同发行版和 JDK 版本中的可用性存在差异。某些商业发行版长期支持,某些版本可能默认关闭或不包含。使用前应以当前运行时和发行版文档为准。

19.1 设计目标

Shenandoah 的核心目标是:

  1. 大部分 GC 工作与应用并发执行;
  2. 支持大堆;
  3. 停顿时间不依赖堆大小;
  4. 尽量减少压缩带来的全局停顿。
应用线程             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

健康信号:

  1. 少量 Degenerated GC;
  2. 没有 Full GC;
  3. 停顿分布稳定;
  4. Live 数据不持续增长;
  5. CPU 有余量支撑并发阶段。

19.8 常见问题

现象 可能原因 处理
Degenerated GC 频繁 堆余量不足、分配过快 增大堆或降低分配
Full GC 回收速度跟不上、配置问题 检查堆、对象和日志
吞吐下降 屏障成本 对比 G1 或 ZGC
CPU 抖动 并发 GC 线程争抢 检查 CPU limit 和线程数
容器 OOM 堆外内存超限 统计 Native、线程、直接内存

19.9 使用建议

  1. 先确认 JDK 发行版明确支持 Shenandoah;
  2. 生产环境固定 -Xms-Xmx
  3. 给堆留出足够余量,避免长期高占用;
  4. 对比测试 G1、ZGC、Shenandoah 的延迟分布;
  5. 关注发行版升级日志,因为 Shenandoah 的屏障和调度策略变化较快;
  6. 不建议只因为“低停顿”就在内存紧张的容器中盲目启用。

本章小结

Shenandoah 通过转发指针、加载引用屏障和并发整理,把对象搬迁与引用修正大部分移出全局停顿。它与 ZGC 解决的是类似问题,但实现路径和版本可用性不同。生产使用前要先确认运行时支持,再通过日志确认是否出现 Degenerated GC、Full GC 和 Allocation Stall,并用同流量压测比较延迟与资源成本。

思考题

  1. 转发指针如何帮助并发整理?
  2. 加载引用屏障和早期转发指针方案有什么取舍?
  3. Degenerated GC 频繁出现说明什么?
  4. Shenandoah 与 ZGC 的可用性判断有什么不同?
  5. 为什么不能只看官方停顿目标选择收集器?