JVM垃圾回收:ZGC与Shenandoah 系统性知识体系(2026超高频)
一、整体定位与发展历程
1.1 低延迟垃圾收集器诞生背景
传统收集器(Serial、Parallel、CMS、G1)在堆内存突破100GB后,停顿时间直线上升——即便是G1,也常常在100ms以上徘徊。这显然不能满足现代云原生和实时系统的要求。于是,设计目标被重新定义:将GC停顿时间控制在亚毫秒级(<10ms),而且不能随堆大小膨胀而增加。实现的核心思路很纯粹:所有耗时操作都并发执行,只在万不得已时才短暂“Stop-The-World”。
1.2 发展历程对比
| 特性 | ZGC | Shenandoah |
|---|---|---|
| 发起方 | Oracle | Red Hat |
| 首次发布 | JDK 11(实验性) | JDK 12(实验性) |
| 正式转正 | JDK 15 | JDK 15 |
| 分代实现 | JDK 21(正式) | JDK 17(实验性),JDK 21(正式) |
| 最新版本 | JDK 23(持续优化) | JDK 23(持续优化) |
| 支持平台 | x86_64、AArch64、Windows、Linux、macOS | 全平台支持 |
二、ZGC核心原理与关键技术
2.1 核心设计思想
ZGC基于Region的内存布局:堆被划分成大小相等的Region(2MB/32MB/256MB,根据堆大小自动选择)。但它真正亮眼的技术“三剑客”是:着色指针(Colored Pointer)——利用64位指针的高几位来存储对象的元数据;读屏障(Load Barrier)——只在读取引用时执行一小段代码,用来处理并发转移;以及多映射内存(Multi-Mapping)——将同一块物理内存映射到多个虚拟地址空间。
2.2 着色指针技术详解
ZGC在64位指针的第42–45位塞进了4个标记位,分别承担不同角色:
- Marked0(M0)——第42位,标记阶段0的存活对象
- Marked1(M1)——第43位,标记阶段1的存活对象
- Remapped(R)——第44位,表示对象已被转移到新地址
- Finalizable(F)——第45位,表示对象有finalize方法需要执行
这么做的好处很明显:对象头里不再需要额外存储标记信息,内存省了;标记和转移可以完全并发进行;转移完成后不需要立即更新所有引用,通过读屏障自动修正即可。
2.3 读屏障技术
当应用线程读取一个对象引用时,ZGC会自动插入读屏障,伪代码逻辑大致如下:
Object load(Object* ref) {
if (ref has Remapped bit set) {
return ref;
}
// 尝试将引用更新到新地址
Object* new_ref = get_forwarded_address(ref);
// CAS更新引用
if (CAS(ref, new_ref)) {
return new_ref;
}
// 其他线程已经更新,重新读取
return load(ref);
}
关键点:只有读操作才会触发屏障,写操作不需要。现代CPU对这种分支预测非常友好,开销极低——这也是ZGC能实现无停顿并发转移的基石。
2.4 ZGC垃圾回收周期
ZGC的回收周期分为四个主要阶段,其中只有两个极短暂的STW窗口:
- 初始标记(STW,<1ms)——标记GC Roots直接引用的对象
- 并发标记——遍历对象图,标记所有存活对象,和应用线程并发执行
- 最终标记(STW,<1ms)——处理并发标记期间的引用变化
- 并发转移——将存活对象复制到新的Region,同时通过读屏障修正引用
- 并发重映射——修正那些没被读屏障处理到的引用(这个步骤可以与下一次标记合并)
三、Shenandoah核心原理与关键技术
3.1 核心设计思想
Shenandoah同样采用基于Region的内存布局,但它的核心技术是Brooks指针——每个对象头里额外保存一个指向自身的转发指针。配合读写屏障,在并发转移时进行重定向。此外,它还引入了一个连接矩阵(Connection Matrix)记录Region之间的引用关系,用来优化跨代引用处理。
3.2 Brooks指针技术
每个对象的头部结构比传统对象多了一个brooks_ptr字段:
struct ObjectHeader {
mark_word; // 标记字
klass_ptr; // 类指针
brooks_ptr; // 转发指针(Shenandoah特有)
};
运作原理:当对象被转移后,原对象的Brooks指针会指向新对象;所有对原对象的访问都会经过Brooks指针自动重定向到新地址。转移完成后,后台线程再逐步更新所有引用。简单说——用本体指向分身,分身搬家后本体指路。
3.3 Shenandoah垃圾回收周期
Shenandoah的回收周期比ZGC要复杂,总共包含9个阶段:
- 初始标记(STW)——标记GC Roots直接引用
- 并发标记——遍历对象图,标记存活对象
- 最终标记(STW)——处理并发标记期间的引用变化
- 并发清理——回收没有存活对象的Region
- 并发准备转移——选择需要转移的Region集合
- 初始转移(STW)——将GC Roots引用更新到新地址
- 并发转移——将存活对象复制到新的Region
- 最终更新引用(STW)——更新所有剩余的引用
- 并发清理——回收转移完成后的旧Region
注意,它的STW阶段多达4个,但每个都控制在亚毫秒级别。
四、低延迟特性深度解析
4.1 低延迟的核心保障
- 全并发执行:标记、转移、清理这些耗时的操作全部与应用线程并发跑,不阻塞业务。
- 停顿时间与堆大小无关:STW只处理GC Roots和引用变化,堆再大也不会增加停顿。
- 增量式处理:大任务被拆成多个小任务,避免一刀切的长暂停。
- 预测性调度:根据应用负载动态调整GC线程的数量和执行时机。
4.2 典型停顿时间对比
| 收集器 | 典型停顿时间 | 最大停顿时间 | 与堆大小关系 |
|---|---|---|---|
| G1 | 100-200ms | 500ms+ | 随堆增大而增加 |
| ZGC(不分代) | <1ms | <10ms | 几乎无关 |
| Shenandoah | <10ms | <20ms | 几乎无关 |
| ZGC(分代,JDK21+) | <1ms | <5ms | 几乎无关 |
4.3 吞吐量与延迟的权衡
ZGC和Shenandoah对比G1,吞吐量会低5%到15%,但延迟降低了一个数量级。两者之间:ZGC平均停顿时间更短,Shenandoah吞吐量略高一筹。决定选谁,关键是看你的场景是否对延迟极度敏感——比如微服务、实时系统、金融交易这些,就该优先考虑ZGC或Shenandoah。
五、JDK21分代ZGC优化(2026超高频考点)
5.1 分代ZGC诞生背景
不分代ZGC有个尴尬的地方:每次回收都要扫描整个堆,堆越大CPU开销越高;不能利用“弱分代假说”——短命对象反复被复制,低效;同时内存碎片问题依旧,时不时得做Full GC。分代设计正好补上这些短板:新生代对象存活率低,回收效率高;老年代对象存活率高,回收频率低。整体吞吐量上来了,CPU开销也降下去了。
5.2 分代ZGC内存布局
堆被划分为两个代:
- 新生代:进一步分为Eden区和Survivor区,采用复制算法回收,频率高
- 老年代:采用标记-整理算法回收,频率低,只在老年代满时才触发
5.3 分代ZGC关键优化点
双标记位循环使用:新生代和老年代用不同的标记位(M0/M1),互不干扰,实现代独立回收。
跨代引用处理:使用卡表(Card Table)记录老年代对新生代的引用。老年代被划分为512字节的卡,当老年代对象引用新生代对象时,对应卡被标记为脏卡。新生代回收时只需扫描脏卡,不用扫描整个老年代。卡表更新通过写屏障实现,开销极低。
晋升机制优化:对象在Survivor区经历若干次回收后晋升到老年代;大对象直接分配在老年代;支持动态调整晋升阈值。
GC触发条件优化:新生代在Eden区满时触发,老年代在占用率达到阈值时触发,并且支持自适应调整。
5.4 分代ZGC性能提升
- 吞吐量提升20%–50%(跟不分代相比)
- CPU开销降低(新生代回收只扫描小部分堆)
- 内存利用率提高,碎片减少
- 停顿时间仍然保持在亚毫秒级,最大不超过5ms
六、ZGC vs Shenandoah 深度对比
6.1 核心技术对比
| 特性 | ZGC | Shenandoah |
|---|---|---|
| 指针技术 | 着色指针(利用指针高位) | Brooks指针(对象头中) |
| 屏障类型 | 仅读屏障 | 读写屏障 |
| 内存布局 | 基于Region | 基于Region |
| 转移方式 | 并发转移 + 读屏障修正 | 并发转移 + Brooks指针重定向 |
| 分代实现 | JDK21正式支持 | JDK21正式支持 |
6.2 性能对比
| 指标 | ZGC | Shenandoah | G1 |
|---|---|---|---|
| 平均停顿时间 | <1ms | <10ms | 100-200ms |
| 最大停顿时间 | <5ms(分代) | <20ms | 500ms+ |
| 吞吐量 | 中等(分代后大幅提升) | 中等偏高 | 高 |
| 内存开销 | 低(着色指针) | 中等(Brooks指针) | 低 |
| CPU开销 | 中等 | 中等偏高 | 低 |
6.3 适用场景
ZGC优先:对延迟要求极高(<10ms)、大堆(100GB+)、微服务/实时系统/金融交易、JDK21及以上。
Shenandoah优先:对吞吐量有一定要求的低延迟应用、需要全平台支持、JDK17及以上。
G1优先:对吞吐量要求高、延迟要求一般(100-200ms)、堆大小较小(<32GB)、或JDK8及以下。
七、调优指南与最佳实践
7.1 ZGC调优参数
# 启用ZGC
-XX:+UseZGC
# 启用分代ZGC(JDK21+,默认开启)
-XX:+ZGenerational
# 设置堆大小
-Xms16g -Xmx16g
# 设置GC线程数(默认等于CPU核心数)
-XX:ConcGCThreads=4
# 设置最大停顿时间目标(默认200ms,ZGC会尽力满足)
-XX:MaxGCPauseMillis=5
# 启用大页支持(提升性能)
-XX:+UseLargePages
7.2 Shenandoah调优参数
# 启用Shenandoah
-XX:+UseShenandoahGC
# 启用分代Shenandoah(JDK21+)
-XX:+ShenandoahGenerational
# 设置GC线程数
-XX:ConcGCThreads=4
# 设置最大停顿时间目标
-XX:MaxGCPauseMillis=10
7.3 最佳实践
- 堆大小:建议设为物理内存的50%–70%,避免过小导致频繁GC。
- GC线程数:默认值通常够用,别设置太多,以免跟应用线程抢CPU。
- 大页支持:强烈建议开启,能显著提升性能。
- JDK版本:尽可能用最新的JDK(JDK21+),分代ZGC带来的收益非常可观。
- 监控指标:重点关注GC停顿时间、GC频率、吞吐量和CPU使用率。
八、2026面试高频考点
8.1 基础概念题
- ZGC和Shenandoah的核心设计思想是什么?
- 着色指针和Brooks指针的区别是什么?
- 读屏障和写屏障在ZGC和Shenandoah中的作用是什么?
- 为什么ZGC的停顿时间与堆大小无关?
8.2 原理深度题
- 详细描述ZGC的垃圾回收周期,指出哪些阶段是STW的?
- 分代ZGC解决了不分代ZGC的哪些问题?
- 分代ZGC的内存布局是怎样的?跨代引用如何处理?
- Shenandoah的连接矩阵有什么作用?
8.3 对比分析题
- ZGC vs Shenandoah vs G1的优缺点对比?
- 分代ZGC相比不分代ZGC有哪些性能提升?
- 什么场景下应该选择ZGC,什么场景下应该选择Shenandoah?
8.4 调优实践题
- 如何开启分代ZGC?有哪些关键调优参数?
- 如果ZGC的停顿时间过长,应该如何排查和调优?
- 大堆场景下使用ZGC有哪些注意事项?
九、未来发展趋势
- 进一步降低停顿:目标是亚微秒级。
- 自动调优增强:根据应用负载自动调整GC参数。
- 硬件加速:利用A VX-512等现代CPU特性加速GC操作。
- 统一垃圾回收框架:JDK正在逐步统一不同收集器的代码框架。
- 弹性堆大小:支持根据应用负载动态调整堆大小。
ZGC与Shenandoah 面试背诵问答卡片(2026超高频)
一、基础概念题(必背)
问题:ZGC和Shenandoah诞生的核心背景与设计目标是什么?
答案:传统收集器(如G1)在100GB+大堆下停顿时间过长(>100ms)且随堆增大而增加。核心设计目标是将GC停顿控制在亚毫秒级(<10ms),且停顿时间与堆大小无关,通过并发执行所有耗时操作实现。
问题:ZGC的三大核心技术是什么?
答案:① 基于Region的内存布局(2MB/32MB/256MB自动选择);② 着色指针(利用64位指针高位存储对象元数据);③ 读屏障(仅在读操作时触发,实现并发转移)。
问题:Shenandoah的三大核心技术是什么?
答案:① 基于Region的内存布局;② Brooks指针(对象头中存储转发指针);③ 读写屏障(读写操作均触发,配合Brooks指针实现重定向)。
问题:为什么ZGC和Shenandoah的停顿时间与堆大小无关?
答案:它们的STW阶段仅处理GC Roots和引用变化,不扫描整个堆。所有耗时的标记、转移、清理操作均与应用线程并发执行,停顿时间只与GC Roots数量和引用变化率有关,与堆总大小无关。
二、核心原理题(高频)
问题:详细解释ZGC的着色指针技术,四个标记位分别是什么?
答案:ZGC利用64位指针的第42–45位存储4个标记位:M0(第42位)标记阶段0的存活对象;M1(第43位)标记阶段1的存活对象;Remapped(第44位)表示对象已被转移到新地址;Finalizable(第45位)表示对象有finalize方法需要执行。优势:无需对象头存储标记位,标记和转移可完全并发,转移后无需立即更新所有引用。
问题:ZGC的读屏障是如何工作的?
答案:当应用线程读取对象引用时,自动插入读屏障:检查引用的Remapped位是否已设置,是则直接返回;否则获取对象的转发地址,通过CAS原子更新引用到新地址,最后返回新地址的对象。特点:仅读操作触发,开销极低,实现了无停顿并发转移。
问题:ZGC的垃圾回收周期包含哪些阶段?哪些是STW的?
答案:共5个阶段,仅2个极短STW阶段:初始标记(STW,<1ms)标记GC Roots直接引用;并发标记遍历对象图标记存活对象;最终标记(STW,<1ms)处理并发标记期间的引用变化;并发转移将存活对象复制到新Region;并发重映射修正剩余未更新的引用(可与下一次标记合并)。
问题:Shenandoah的Brooks指针是如何工作的?
答案:每个对象头额外存储一个转发指针,初始指向自身。当对象被转移时,原对象的Brooks指针指向新对象。所有对原对象的访问都会通过Brooks指针自动重定向到新对象,转移完成后后台线程逐步更新所有引用。
问题:Shenandoah的垃圾回收周期包含哪些关键STW阶段?
答案:共4个STW阶段,均为亚毫秒级:初始标记(标记GC Roots直接引用)、最终标记(处理并发标记的引用变化)、初始转移(更新GC Roots引用到新地址)、最终更新引用(更新所有剩余引用)。
三、JDK21分代ZGC专题(2026超高频)
问题:不分代ZGC存在哪些核心痛点?分代ZGC解决了什么问题?
答案:不分代ZGC痛点:每次回收扫描整个堆,大堆CPU开销高;无法利用弱分代假说,大量短生命周期对象被重复复制;内存碎片问题仍存在,需定期Full GC。分代ZGC解决:吞吐量提升20%–50%,CPU开销降低,内存利用率提高,同时保持亚毫秒级停顿。
问题:分代ZGC的内存布局是怎样的?
答案:将堆划分为两个独立代:新生代(Eden+Survivor)采用复制算法,回收频率高;老年代采用标记-整理算法,回收频率低。两代使用不同的标记位(M0/M1)循环,实现代独立回收。
问题:分代ZGC如何处理跨代引用?
答案:使用卡表(Card Table)记录老年代对新生代的引用。老年代被划分为512字节的卡,当老年代对象引用新生代对象时,对应卡被标记为脏卡。新生代回收时只需扫描脏卡,无需扫描整个老年代,卡表更新通过写屏障实现。
问题:分代ZGC相比不分代ZGC有哪些关键性能提升?
答案:吞吐量提升20%–50%;CPU使用率降低约30%;内存碎片显著减少,内存利用率提高;最大停顿时间从<10ms进一步降低到<5ms;大幅降低了大堆场景下的Full GC概率。
问题:JDK21中分代ZGC是默认开启的吗?如何手动开启/关闭?
答案:JDK21及以上版本中,分代ZGC默认开启。手动开启:-XX:+ZGenerational;手动关闭(使用不分代ZGC):-XX:-ZGenerational。
四、对比分析题(高频)
问题:ZGC和Shenandoah的核心技术区别是什么?
答案:
| 维度 | ZGC | Shenandoah |
|---|---|---|
| 指针技术 | 着色指针(利用指针高位) | Brooks指针(对象头中) |
| 屏障类型 | 仅读屏障 | 读写屏障 |
| 内存开销 | 低(无额外对象头开销) | 中等(每个对象多一个指针) |
| 平均停顿 | <1ms | <10ms |
| 吞吐量 | 中等(分代后大幅提升) | 中等偏高 |
问题:ZGC、Shenandoah和G1的适用场景分别是什么?
答案:ZGC适合对延迟要求极高(<10ms)的大堆(100GB+)场景,如金融交易、实时系统、微服务,推荐JDK21+;Shenandoah适合对吞吐量有一定要求的低延迟应用,需要全平台支持,推荐JDK21+;G1适合对吞吐量要求高、延迟要求一般(100-200ms)的中小堆(<32GB)场景,或JDK8及以下。
问题:分代ZGC和分代Shenandoah哪个更推荐使用?
答案:JDK21及以上版本优先推荐分代ZGC。它的平均停顿时间更短(<1ms vs <10ms),内存开销更低,且经过Oracle更充分的优化和测试。分代Shenandoah适合需要全平台支持或对Red Hat生态有依赖的场景。
五、调优实践题(高频)
问题:开启ZGC的核心JVM参数有哪些?
答案:
# 启用ZGC(JDK15+)
-XX:+UseZGC
# 启用分代ZGC(JDK21+,默认开启)
-XX:+ZGenerational
# 设置堆大小(建议物理内存的50%-70%)
-Xms16g -Xmx16g
# 设置GC线程数(默认等于CPU核心数)
-XX:ConcGCThreads=4
# 设置最大停顿时间目标(默认200ms,ZGC会尽力满足)
-XX:MaxGCPauseMillis=5
# 启用大页支持(强烈推荐,性能提升显著)
-XX:+UseLargePages
问题:如果ZGC的停顿时间过长,应该如何排查和调优?
答案:检查GC日志,确认是哪个STW阶段耗时过长。若初始标记/最终标记过长,减少GC Roots数量(如减少线程数);若并发阶段跟不上应用速度,增加ConcGCThreads;适当调大堆大小减少GC频率;启用大页支持降低内存访问延迟;升级到最新JDK版本(JDK21+分代ZGC停顿更短)。
问题:大堆场景下使用ZGC有哪些最佳实践?
答案:必须使用JDK21及以上版本的分代ZGC;堆大小建议设为物理内存的50%–70%;启用大页支持(透明大页或巨页);GC线程数设为CPU核心数的1/4–1/2,避免与应用线程竞争;不要设置过小的MaxGCPauseMillis,否则会导致GC频率过高;重点监控GC频率、停顿时间和CPU使用率。
