Shenandoah GC的核心目标是实现亚毫秒级(0.5–3ms)可预测极短STW,通过全并发执行标记、疏散等操作,仅保留两个极短安全点,并依赖Brooks指针实现对象转发,彻底放弃分代假设,采用统一Region管理与连接矩阵,适用于P999延迟敏感且无法使用ZGC的场景。

Shenandoah GC 的核心目标是亚毫秒级 STW,不是“低延迟”而是“可预测的极短停顿”
它不追求吞吐优先,也不妥协于大堆带来的暂停增长。实测中,2GB 堆和 200GB 堆在 JDK17+ 上的典型 STW 都能压到 0.5–3ms 范围内。关键在于:所有耗时操作(标记、疏散、引用更新、类卸载、线程栈扫描)全部并发执行,仅保留两个极短的初始和最终安全点。这和 G1 的 Mixed GC 暂停动辄几十毫秒有本质区别。
Brooks Pointer 是它实现并发搬迁的唯一依赖机制
每个对象头额外增加一个 fwdptr 字段(8 字节),初始指向自己;对象被 GC 线程复制到新地址后,通过 CAS 原子更新该指针指向新位置。后续所有对该对象的读写访问,都由写屏障拦截并转发到新地址。这意味着:
- 无需修改应用线程的栈帧或寄存器中的旧引用(ZGC 的染色指针需要读屏障+TLAB重填)
- 但每个对象多占 8 字节,对小对象密集型场景(如大量
Integer、Boolean)内存开销明显 - 写屏障开销比读屏障略高,尤其在高频 setter 场景下可能影响单线程吞吐
它彻底放弃分代假设,Region 管理更简单但回收粒度更粗
Shenandoah 不区分年轻代/老年代,整个堆统一划分为固定大小 Region(默认 2MB)。GC 周期启动后,会并发扫描所有 Region 并决定哪些需要疏散。这种设计带来两个直接后果:
- 没有 Minor GC 和 Major GC 的概念,也就不存在“晋升失败”或“担保失败”这类 G1 特有的异常路径
- 连接矩阵(Connection Matrix)替代卡表,节省记忆集内存,但跨 Region 引用扫描范围更大,依赖并发处理能力
- 对缓存局部性不友好——对象重分配后物理位置完全随机,可能加剧 CPU cache miss
启用和调优必须绕过实验性开关,且 OpenJDK 构建版本不能错
从 JDK15 开始 -XX:+UseShenandoahGC 已不再是实验选项,但前提是使用官方支持的 OpenJDK 构建(如 builds.shipilev.net 提供的版本)。Oracle JDK 或某些 Linux 发行版自带的 OpenJDK 往往不含 Shenandoah。常见踩坑点包括:
- 误用
java -version显示 “OpenJDK 17” 就认为可用,实际未启用 Shenandoah —— 必须看到shenandoah出现在java -XX:+PrintCommandLineFlags -version输出里 - 漏加
-XX:+UnlockExperimentalVMOptions(JDK12–14 必须,JDK15+ 可省略) - 在容器环境里未显式设置
-XX:ShenandoahHeapRegionSize=,导致自动推导出不合适的 Region 大小,引发频繁的 Region 分裂或浪费
真正难的是权衡:当业务对 P999 延迟极度敏感(比如实时匹配、行情推送),又无法接受 ZGC 对大页内存和 NUMA 的强依赖时,Shenandoah 才值得投入调优。它的停顿确实稳定,但那 8 字节/对象的隐性成本和写屏障的持续开销,容易在压测中突然暴露为吞吐断崖。


















