G1\Shenandoah\ZGC

G1\Shenandoah\ZGC

总结

  • G1垃圾收集器初始标记(并行,短暂stop the world)、并发标记(并发)、最终标记(并行,短暂stop the world)、筛选回收(即复制对象)(并行,较长stop the world)

  • Shenandoah垃圾收集器初始标记(并行,短暂stop the world)、并发标记(并发)、最终标记(并行,短暂stop the world)、筛选回收(即复制对象)(并发)

    • 并发回收整理:转发指针(旧引用访问复制后的对象)、连接矩阵(分析和更新引用))
  • ZGC垃圾收集器初始标记(并行,短暂stop the world)、并发标记(并发)、最终标记(并行,短暂stop the world)、筛选回收(即复制对象)(并发)

    • 并发回收整理:重分配集(用来判定回收哪些Region)、读屏障(ZGC使用了读屏障来完成指针的“自愈”)、染色指针(旧引用访问复制后的对象)、内存多重映射(操作系统支持)、NUMA(ZGC会优先在自己的本地内存上分配对象))
阶段 G1 Shenandoah ZGC
初始标记 并行,短暂 STW 并行,短暂 STW 并行,短暂 STW
并发标记 并发 并发 并发
最终标记 并行,短暂 STW 并行,短暂 STW 并行,短暂 STW
筛选回收(复制对象) 并行,较长 STW 并发 并发

关键补充差异

虽然 Shenandoah 和 ZGC 都是并发复制,但实现机制不同:

Shenandoah 的并发复制

  • 使用 读屏障 + 写屏障(JDK 8/11)/ 引用访问屏障(JDK 17+)
  • 复制对象时,在旧对象头中写入转发指针
  • 用户线程通过读屏障检测对象是否被移动,按需修正
  • Brooks Pointer 技术:每个对象头保留一个指针字段,移动后指向新地址

ZGC 的并发复制

  • 使用 染色指针 + 读屏障
  • 颜色信息直接编码在 64 位指针的高 4 位(JDK 11-15)或高 2 位(JDK 16+ 指针压缩优化后调整)
  • 不需要在对象头中存储转发指针(节省内存)
  • 支持 多重映射(Multiple Mapping),同一物理地址映射到多个虚拟地址,便于颜色切换

为什么 G1 不把复制也变成并发?

原因 说明
实现复杂度 并发复制需要屏障技术,G1 设计时(JDK 7)优先级是平衡吞吐量和停顿
Remembered Set 维护 G1 的 RSet 本就复杂,并发复制会使跨 Region 引用维护变得极其困难
目标定位 G1 目标是替代 CMS,提供可预测的停顿(200ms 级别),并非亚毫秒级
屏障开销 读屏障/写屏障会降低吞吐量,G1 选择了更轻量的 SATB 写屏障

一句话总结对比

  • G1:只有标记并发,复制还是要 STW(所以停顿相对较长)
  • Shenandoah:标记和复制都并发,但要额外维护转发指针
  • ZGC:标记和复制都并发,用染色指针实现,不占对象头额外空间

选择建议

场景 推荐 原因
更在意物理内存占用 ZGC 不增加对象头,物理内存占用更低
追求最低停顿(<1ms) ZGC 停顿更稳定
吞吐量优先但需要低停顿 ZGC 略高吞吐量
无法忍受染色指针的复杂性 Shenandoah 实现更“传统”,调试和理解更简单
JDK 11 且需要生产稳定 Shenandoah JDK 11 的 ZGC 还是实验特性,Shenandoah 已稳定

Shenandoah转发指针

转发指针是 Shenandoah GC 实现并发复制的核心技术。

简单说:它是一个存储在对象头中的8字节指针字段,用于在对象被移动后,指向该对象的新地址

  • JDK 21 及以后:转发指针被“藏”起来了(压缩或内嵌),但机制依然在。转发指针Brooks Pointer 嵌入到 Mark Word 的高位中,不再独立占用 8 字节。

对象搬家后,旧对象里留一张"新地址纸条",所有访问都通过这张纸条跳转到新家。

正常情况(对象未移动):
┌─────────────────┐
│   Mark Word     │
├─────────────────┤
│  Class Pointer  │
├─────────────────┤
│ Brooks Pointer  │──────┐
├─────────────────┤      │ 指向自己
│   实例字段...    │      │ (self pointer)
└─────────────────┘◄─────┘

对象被移动后(并发复制):
┌─────────────────┐        ┌─────────────────┐
│   Mark Word     │        │   Mark Word     │
├─────────────────┤        ├─────────────────┤
│  Class Pointer  │        │  Class Pointer  │
├─────────────────┤        ├─────────────────┤
│ Brooks Pointer  │────────┼→│ Brooks Pointer │──────┐
├─────────────────┤        ├─────────────────┤      │ 指向新对象
│   旧对象(已空) │        │   实例字段...    │      │
└─────────────────┘        └─────────────────┘◄─────┘

用户线程持有一开始指向“旧对象”的指针,
但通过 Brooks Pointer 的间接寻址,最终访问到“新对象”。
  • JDK 11 及以前:转发指针是显式的 8 字节,用 JOL 一眼就能看到。
  • JDK 21 及以后:转发指针被“藏”起来了(压缩或内嵌),但机制依然在。为了观察,要么降级 JDK 版本,要么使用更底层的调试工具来捕捉对象被搬运后的瞬间内存变化。
    • 转发指针Brooks Pointer 嵌入到 Mark Word 的高位中,不再独立占用 8 字节。

测试疑问

我用jdk11、jdk21测试下来,都没看到Brooks Pointer的占用,具体怎么才能看,目前真不知道。

// 无论用8、11、21版本的jdk打印都是这个。都没看到Brooks Pointer的占用。
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

ZGC染色指针

染色指针(Colored Pointer) 是 ZGC 的核心技术,通过在 指针本身的空闲位 中存储 GC 状态信息,让 GC 在不访问对象头的情况下就能知道对象的状态。

基本原理

在 64 位系统上,指针是 64 位(8 字节)的。但实际上我们并不需要全部 64 位来寻址(比如 4TB 堆只需要 42 位)。ZGC 利用高位空闲位来存储颜色信息。

JDK 11-15 的实现(64 位指针布局):

63-42位 (22位) | 41-18位 (24位) | 17-1位 (17位) | 0位
   颜色位        实际地址(堆内偏移)    保留        finalizable位

指针本身的空闲位是指Student s = new Student(),s的这个地址的空闲位吗?:

  • s 这个变量里存储的那个 64 位的内存地址值,它本身的一些高位(比如第 42-63 位)就是染色指针所说的“空闲位”。
  • 染色指针染的不是对象,而是指向对象的那个“指针变量”里存储的地址数值的高位空闲位。

堆内存上限

在 64 位系统上,指针是 64 位(8 字节)的。但实际上我们并不需要全部 64 位来寻址(比如 4TB 堆只需要 42 位)。 这个4TB 堆指的是jvm的堆内存还是系统内存?

指的是 JVM 的堆内存,不是系统物理内存。

JDK 版本 ZGC 最大堆 寻址所需位数 颜色位占用
JDK 11-15 4TB 42 位 22 位
JDK 16+ 16TB 44 位 20 位(优化后)

JDK 16 之后,通过压缩指针优化,将最大堆提升到 16TB(需要 44 位寻址)。

分代模式

无论是 Shenandoah 还是 ZGC,现在默认都已经启用分代模式。虽然它们最初确实是默认非分代的,但为了获得更好的性能表现和更低的维护成本,OpenJDK 团队已相继将默认行为切换为分代模式

垃圾收集器 当前默认模式 如何显式启用分代模式 如何切换回非分代模式 (不推荐)
ZGC 分代模式 (从 JDK 21 开始引入,JDK 23 起成为默认) -XX:+UseZGC (无需额外参数) -XX:+UseZGC -XX:-ZGenerational (会收到弃用警告,未来版本将移除)
Shenandoah 分代模式 (计划中,目标 JDK 24/25 使其成为默认) -XX:+UseShenandoahGC (无需额外参数) -XX:+UseShenandoahGC -XX:ShenandoahGCMode=satb (会收到弃用警告,未来版本将移除)

ZGC:

  • JDK 11-20:只需指定 -XX:+UseZGC 即可启用非分代模式
  • JDK 21-22:使用 -XX:+UseZGC -XX:+ZGenerational 开启分代模式(当时为可选)。
  • JDK 23+:使用 -XX:+UseZGC 就会默认启用分代模式,无需额外参数

Shenandoah

  • JDK 11-23:非分代模式。
  • JDK 24:使用 -XX:+UseShenandoahGC -XX:+UnlockExperimentalVMOptions XX:ShenandoahGCMode=generational 开启分代模式(当时为可选)。
  • JDK 25+:计划默认使用 -XX:+UseShenandoahGC 就会默认启用分代模式,无需额外参数

🔍 为什么会发生这种变化?

这个转变的背后是技术的必然选择,主要有两个原因:

  1. 性能更优:分代收集基于“弱分代假说”(大部分对象朝生夕灭),能更频繁地回收新生代区域。这带来了更低的并发标记开销、更高的分配吞吐量以及更小的内存占用,对绝大多数应用都是更好的解决方案。
  2. 降低维护成本:同时维护分代和非分代两套代码,会严重拖累新功能的开发。因此,OpenJDK 团队决定统一为分代模式,并最终移除老的非分代代码,以减轻长期的维护负担。

💡 影响及建议

  • 默认就是最佳选择:对于绝大多数Java应用来说,直接使用 -XX:+UseZGC-XX:+UseShenandoahGC 启动,让JVM采用默认的分代模式就是最佳实践。你不需要再像早期版本那样去额外指定分代参数。
  • 注意启动参数的变化:如果你之前为ZGC手动添加过 -XX:+ZGenerational 或为 Shenandoah 添加过 -XX:ShenandoahGCMode=generational 来开启分代模式,可以放心地移除这些参数,因为现在不加这些参数才是分代模式,加上了反而会收到参数已过时或不存在的报错。
  • 测试验证:在升级JDK版本后,建议在测试环境中充分验证应用的性能和延迟表现。虽然分代模式性能更优,但对于一些极其特殊的、非分代友好的工作负载,也可能需要重新评估
posted @ 2026-05-22 15:31  deyang  阅读(23)  评论(0)    收藏  举报