JDK 17 的 GC:G1 默认,ZGC 与 Shenandoah 转正
学完你要能自己回答
- G1 的 Region、Remembered Set 与 Mixed GC 分别解决什么问题?
- ZGC 的染色指针与 load barrier 如何把停顿压到毫秒级?代价是什么?
- Shenandoah 与 ZGC 的实现路线有什么不同?
- MaxGCPauseMillis 是 SLA 吗?设得过低会发生什么?
- 17 上应该怎么在 G1 / ZGC / Shenandoah 之间做选择?
三种收集器的位置01
JDK 17 提供 Serial、Parallel、G1、ZGC、Shenandoah 和 Epsilon。默认是 G1;ZGC 与 Shenandoah 在 15 从实验转为正式,17 不再需要解锁参数。
| 收集器 | 17 状态 | 核心目标 | 典型代价 |
|---|---|---|---|
| G1 | 默认 | 可预测的停顿 + 较好吞吐 | Remembered Set 与写屏障开销 |
| ZGC | 15 正式 | 极低停顿,支持大堆 | 染色指针、load barrier、内存占用 |
| Shenandoah | 15 正式 | 低停顿,并发回收 | Brooks 指针、读屏障、额外对象头 |
| Parallel | 可用 | 吞吐优先 | 停顿随堆增长 |
| Serial | 可用 | 小堆、单核 | 全停顿,不适合大堆服务 |
「低停顿」不等于「更快」。并发收集器把工作放到应用线程之外,会消耗 CPU 和内存带宽;停顿缩短了,单位时间内的吞吐可能下降。选择收集器之前,先明确你优化的是尾延迟还是吞吐。
停顿目标不是 SLA02
G1 的 -XX:MaxGCPauseMillis 默认 200ms(本机 JDK 17 实测)。它是一个目标,不是硬性上限;G1 会据此调整年轻代大小和回收频率,但无法保证每次停顿都不超过它。
- 把目标设得过低,G1 会缩小年轻代、增加 GC 次数,吞吐可能明显下降。
- 堆很大、对象分配率很高时,单次停顿仍可能超过目标。
- ZGC / Shenandoah 的低停顿来自并发标记与并发重定位,不代表应用线程完全不受影响。
- 评估 GC 要看延迟分位(p95 / p99)、GC 频率、CPU 占用和分配率,不只看平均值。
先猜再看把 MaxGCPauseMillis 设成 10ms,服务延迟就会变好吗?
G1:区域化与增量回收01
G1 把堆划分成大小相等的 Region(1–32MB,按堆大小取 2 的幂)。对象分配在 Region 中,年轻代只是 Region 的一个逻辑集合,不再要求物理连续。
- Young GC:回收 Eden 与 Survivor Region,存活对象复制到新的 Survivor 或晋升老年代。
- 并发标记:老年代占用达到阈值后启动,使用 SATB(Snapshot-At-The-Beginning)记录并发期间的引用变化。
- Mixed GC:在年轻代回收之外,挑选垃圾比例高的老年代 Region 一起回收。
- Full GC:并发标记或复制失败时的兜底,通常是全停顿,应尽量避免。
跨 Region 引用由 Remembered Set 记录,避免每次 Young GC 扫描整个老年代。对象大于 Region 一半时按 Humongous 处理,分配在连续的 Humongous Region 中;大数组、大字符串会放大这种开销。
| 概念 | 解决的问题 | 代价 |
|---|---|---|
| Region | 把堆切成可增量回收的单元 | 大对象需要连续 Region |
| Remembered Set | 只扫描有跨代引用的 Region | 写屏障与内存开销 |
| SATB | 并发标记期间保持快照语义 | 标记阶段额外 CPU |
| Mixed GC | 逐步回收老年代,避免全堆停顿 | 回收选择与复制成本 |
ZGC:染色指针与并发重定位02
ZGC 把标记信息放在指针里(染色指针),而不是对象头。应用线程读取引用时经过 load barrier,根据指针颜色决定是否需要帮忙完成标记或重定位。
- 并发标记:遍历对象图,把可达性信息写进指针的颜色位。
- 并发重定位:把存活对象复制到新 Region,旧地址通过转发表记录。
- 并发重映射:修正仍指向旧地址的引用,使旧空间可回收。
- 整个过程只有极短的停顿用于根扫描等关键步骤,大部分工作与应用线程并发。
- ZGC 在 17 是单代收集器:没有年轻代/老年代之分,所有周期都针对整个堆。
- 染色指针依赖 64 位地址空间;ZGC 在 17 不支持压缩普通对象指针,引用占用更多内存。
- load barrier 让每次引用读取都多了一点开销;低停顿的代价是吞吐与内存。
- JEP 377 的目标是把停顿控制在 10ms 以内,但这是设计目标,不是任何负载下的保证。
Shenandoah:Brooks 指针03
Shenandoah 也做并发标记与并发 evacuation,但它用 Brooks 指针实现对象转发:每个对象多一个转发指针字,读屏障在访问对象时检查是否已被移动。
| 维度 | ZGC | Shenandoah |
|---|---|---|
| 标记信息位置 | 引用指针的颜色位 | 对象头 / Brooks 指针 |
| 读屏障 | load barrier 检查指针颜色 | 读屏障检查转发指针 |
| 压缩指针 | 17 不支持 | 17 支持(取决于平台与堆) |
| 分代 | 17 单代 | 17 单代 |
| 主要代价 | 地址空间与内存占用 | 每对象额外指针、读屏障开销 |
两者都追求低停顿,但实现路线不同:ZGC 把元数据放进指针,Shenandoah 把转发状态放进对象。选择时不能只看纸面参数,要看供应商支持、堆规模、分配模式和实测尾延迟。
-XX:+UseShenandoahGC 启动(本机验证)。但是否可用取决于发行版与平台,部署前先在目标镜像里确认。选择表01
| 场景 | 优先选择 | 理由 |
|---|---|---|
| 通用 Web 服务、堆 < 16GB | G1(默认) | 吞吐与停顿平衡,调优资料多 |
| 堆很大、尾延迟要求硬 | ZGC | 并发回收,停顿与堆大小关系弱 |
| 低停顿 + 希望保留压缩指针 | Shenandoah | Brooks 指针路线,支持压缩指针 |
| 批处理、吞吐优先 | Parallel | 减少并发开销,充分利用 CPU |
| 小堆、单核、启动快 | Serial | 实现简单,开销小 |
| 极端低延迟演示 / 测试 | Epsilon | 不回收,仅分配;不适合生产常驻 |
这张表只是起点。真正选择前,用生产相似的堆大小、分配率和对象生命周期做压测,比较 p99 延迟、吞吐、GC CPU 占用和内存足迹。
-XX:+UseG1GC -XX:+UseZGC 会让 JVM 报冲突;一次只选一个。参数与验证02
# 默认:G1
java -Xmx256m -cp out GcInfo
# maxHeapMB=256
# collector=G1 Young Generation
# collector=G1 Old Generation
# ZGC(17 单代)
java -XX:+UseZGC -Xmx256m -cp out GcInfo
# maxHeapMB=256
# collector=ZGC Cycles
# collector=ZGC Pauses
# Shenandoah
java -XX:+UseShenandoahGC -Xmx256m -cp out GcInfo
# maxHeapMB=256
# collector=Shenandoah Pauses
# collector=Shenandoah Cycles
$ java -XX:+PrintFlagsFinal -version | grep -E "UseG1GC|MaxGCPauseMillis"
uintx MaxGCPauseMillis = 200 {product} {default}
bool UseG1GC = true {product} {ergonomic}
- 用
ManagementFactory.getGarbageCollectorMXBeans()或 GC 日志确认实际生效的收集器,不要只看启动参数。 -Xlog:gc*:file=gc.log:time,uptime,level,tags是 17 推荐的日志写法,替代已弃用的 PrintGCDetails。-Xms与-Xmx设成相同值可以避免堆伸缩带来的额外停顿,但是否合适取决于部署密度。- ZGC / Shenandoah 的并发线程会占用 CPU;容器里要设置合理的 CPU limit,避免 GC 线程与应用线程互相挤压。
排障顺序03
- 先看 GC 日志:是 Young GC 频繁、Mixed GC 太慢,还是出现了 Full GC。
- 看分配率与晋升率:年轻代过小会导致对象过早晋升,老年代压力增大。
- 查大对象:Humongous 分配、超大数组、缓存无上限都会让 G1 退化。
- 查容器限制:堆设置超过 cgroup 限制会被 OOM Killer 杀掉,不是 GC 问题。
- 需要低延迟再评估 ZGC / Shenandoah;先确认瓶颈真的在 GC 停顿。
- 调整后重新压测,比较 p99、吞吐和 CPU,不要只看单次 GC 时间。
| 现象 | 可能原因 | 先查什么 |
|---|---|---|
| Full GC 频繁 | 老年代增长快、并发标记跟不上 | 分配率、晋升率、老年代占用曲线 |
| Young GC 很频繁 | 年轻代过小、分配率过高 | Eden 大小、对象分配热点 |
| 单次停顿很长 | Mixed GC 回收 Region 过多、Humongous | Mixed GC 日志、大对象 |
| 容器被 kill | 堆 + 元空间 + 线程栈超过 limit | RSS、cgroup 限制、-Xmx |
| 吞吐下降但停顿短 | 并发收集器占用 CPU | GC 线程 CPU、应用 CPU |