zgc和shenandoah是放弃分代假设、依赖os/hardware协同实现亚毫秒stw的新一代gc,选择取决于内核版本、堆大小及内存容忍度;zgc需linux≥4.14支持大页重映射,shenandoah需禁用透明大页,二者屏障机制不同导致内存与cpu开销此消彼长。

ZGC 和 Shenandoah 都不是“升级版 G1”,而是彻底放弃分代假设、用硬件/OS协同机制实现亚毫秒级 STW 的新一代 GC;选哪个不看版本新旧,而要看你跑在什么内核上、堆有多大、能不能接受多占 10%~15% 内存。
怎么启用 ZGC 或 Shenandoah?别漏掉前提条件
两者都不是加个 -XX:+UseZGC 或 -XX:+UseShenandoahGC 就能稳跑的。JVM 会检测底层能力,缺了就降级甚至报错:
- ZGC 要求 Linux 内核 ≥ 4.14(大页重映射支持),否则 fallback 到保守模式,
Initial Mark和Final Mark停顿可能飙升到几十毫秒 - Shenandoah 在内核 userfaultfd,遇到内存分配失败会直接触发
Full GC,而不是并发 degenerated GC - 必须显式指定最大堆:
-Xmx16g—— ZGC 不支持动态伸缩;Shenandoah 虽支持,但扩容本身会触发 STW - 别混搭参数:
-XX:+UseZGC -XX:+UseG1GC这种写法 JVM 直接拒绝;-XX:+UseStringDeduplication对 ZGC/Shenandoah 无效,会被忽略
为什么日志里看不到 “Young GC”?这不代表没压力
因为 ZGC 和 Shenandoah 默认关闭分代,整个堆统一标记+回收,所以 GC 日志中不会出现 Young GC 或 Mixed GC 字样。但这绝不意味着对象分配没问题:
- TLAB(Thread Local Allocation Buffer)依然存在,只是不再按“年轻代”逻辑管理;如果
Allocation Stall频发,说明线程频繁申请不到本地缓冲区,本质是分配速率压过了并发回收节奏 - 没有 Remembered Set,也就没有跨代引用跟踪开销,但代价是每次 GC 都要 scan 全堆 —— 所以堆越大,CPU 时间越长,虽然 STW 不涨,吞吐可能掉
- 观察
gc.log时重点看Pause Init Mark、Pause Final Mark(ZGC)或Init Mark、Final Mark(Shenandoah)的耗时总和,它们通常控制在 2ms,先查内核版本和透明大页(echo never > /sys/kernel/mm/transparent_hugepage/enabled)
Shenandoah 的 Brooks 指针 vs ZGC 的染色指针:内存与 CPU 怎么换算?
两者都靠屏障(barrier)实现并发移动,但实现路径不同,直接影响资源占用:
- Shenandoah 在每个对象头额外加两个字段(
fwdptr),用于指向重定位后地址;这带来约 10%~15% 的堆内存开销,但屏障逻辑相对轻量,读屏障只在对象被转发且堆有 forwarded 对象时触发 - ZGC 把标记位、重定位位等直接编码进 64 位指针低 bits(染色),不改对象结构,内存更省;但要求 OS 支持虚拟内存多重映射(
mmapwithMADV_DONTNEED等),且每次对象访问都要走 load barrier 解码指针,CPU 开销略高 - 实测中:容器环境(内存受限、内核较新但常禁用大页)倾向 Shenandoah;裸金属/VM 上跑 TB 级堆(如实时风控引擎)、CPU 资源富余,ZGC 吞吐更稳
真正难的不是配对参数,而是理解:ZGC 和 Shenandoah 把 GC 停顿从“堆大小函数”变成了“OS 能力函数”。内核没跟上,再新的 JDK 也救不了 STW。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











