八股文解析
JVM 的 G1 和 ZGC 有什么区别?怎么选?
一句话结论
延迟敏感、大堆场景选 ZGC;吞吐优先、JDK 8 存量系统选 G1。
面试标准答法
1. 定位与演进
- G1 (Garbage-First):JDK 7 引入、JDK 9 成为默认收集器。基于 Region 化内存布局 + SATB (Snapshot-At-The-Beginning) 并发标记,目标是可预测的停顿时间(软实时)。
- ZGC (Z Garbage Collector):JDK 11 实验、JDK 15 转正。基于 Region + 着色指针 (Colored Pointers) + 读屏障 (Load Barrier),目标是亚毫秒级停顿,且停顿时间不随堆大小增长。
2. 核心机制对比(分层讲)
第一层:内存布局
- G1:堆被划分为约 2048 个等大 Region(1MB~32MB),逻辑上分 Eden / Survivor / Old / Humongous(大对象直接用连续 Region 存放)。Region 之间通过 Remembered Set (RSet) 记录跨 Region 引用,维护成本高。
- ZGC:堆被划分为 ZPage(动态大小,小/中/大三种),无固定分代(JDK 21 起支持分代 ZGC)。核心是逻辑上的多重视图(M0 / M1 / Remapped),通过指针自愈实现引用修正,无 RSet。
第二层:并发标记与转移
- G1 的并发标记:
- 使用 SATB 保证并发标记期间对象图一致性(记录 GC 开始时的快照,新增引用入队)。
- 转移阶段(Evacuation):并发转移存活对象到新 Region,但转移过程中需要 STW(Young GC 完全 STW,Mixed GC 部分并发)。
- 停顿预测模型(Pause Prediction Model):根据历史数据动态调整 Region 回收数量,尝试满足
-XX:MaxGCPauseMillis目标。
- ZGC 的并发转移:
- 使用着色指针:指针的高位(64 位系统中的 18 位)存储 GC 元数据(Marked0 / Marked1 / Remapped / Finalizable)。
- 读屏障 (Load Barrier):每次从堆中加载引用时,检查指针颜色。若对象处于"转移中"状态,则通过转发指针 (Forwarding Pointer) 立即重定向到新地址,并自愈(更新当前引用),实现完全并发转移。
- 无 SATB,标记阶段通过指针颜色切换(M0↔M1)区分并发周期。
第三层:停顿时间构成
- G1:STW 发生在 Young GC(停止应用)、Mixed GC 的初始标记/最终标记/清理阶段。停顿通常在 10ms~100ms 量级,受 RSet 扫描和 Region 数量影响。
- ZGC:STW 仅发生在根扫描(线程栈、JNI 引用等)和初始化阶段。并发标记、并发转移、并发重映射(Remap,可合并到下一次标记)全部与应用线程并发。停顿稳定在 <1ms(通常 0.2ms~0.5ms),与堆大小无关。
第四层:关键参数与 JDK 版本
| 维度 | G1 | ZGC |
|---|---|---|
| JDK 版本 | JDK 9+ 默认 | JDK 15+ 正式(JDK 21 支持分代) |
| 核心参数 | -XX:+UseG1GC | -XX:+UseZGC |
| 停顿目标 | -XX:MaxGCPauseMillis=200(默认 200ms) | -XX:ZCollectionInterval(可选) |
| 堆上限 | 理论无上限,但 >64GB 时 RSet 开销大 | 理论无上限(支持 16TB) |
| 内存开销 | RSet + 2 个 BitMap(标记位图) | 着色指针(无额外数据结构)+ 转发表,但指针压缩不可用(内存开销略高) |
| 大对象处理 | Humongous Region,直接分配 | 大 ZPage,直接分配 |
| 分代支持 | 是(逻辑分代) | JDK 21+ 分代 ZGC(默认开启) |
对比表格:适用场景 / 优缺点
| 维度 | G1 | ZGC |
|---|---|---|
| 适用场景 | 中小堆(4GB~32GB)、JDK 8 存量系统、吞吐敏感型服务 | 大堆(>32GB)、延迟敏感型服务(交易、搜索、实时推荐) |
| 优点 | ① 吞吐量高(并发线程少)② 参数成熟、调优经验丰富 ③ JDK 8 可直接使用 | ① 停顿时间极低且恒定 ② 堆越大优势越明显 ③ 无 RSet,内存管理简单 |
| 缺点 | ① 停顿时间随堆增大而增加 ② RSet 维护开销大 ③ 大堆下 Full GC 风险高 | ① 吞吐量略低(读屏障开销约 5%~15%) ② JDK 11 以下不可用 ③ 调优资料相对少 |
| 典型参数 | -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:ParallelGCThreads=8 | -XX:+UseZGC -XX:ZAllocationSpikeTolerance=2 |
常见追问及要点
| 追问 | 核心要点 |
|---|---|
| ZGC 的着色指针为什么能保证并发安全? | ① 指针高位存状态,通过 CAS 原子更新颜色 ② 读屏障在引用加载时检查并自愈 ③ 转发指针保证转移期间对象唯一地址 ④ 无锁实现,靠内存屏障保证可见性 |
| G1 的 Full GC 什么时候发生?怎么避免? | ① 并发标记后存活对象过多,Mixed GC 来不及回收 ② Humongous 分配失败 ③ 避免方法:控制大对象、调整 -XX:G1HeapRegionSize、增加 -XX:G1ReservePercent、使用 -XX:+G1HeapWastePercent 调优 |
| ZGC 为什么吞吐量比 G1 低? | ① 读屏障在每次引用加载时执行(热点路径开销) ② 并发线程数量更多(标记/转移/重映射三阶段并发) ③ 无分代时全堆扫描,分代 ZGC 改善后仍略低于 G1 |
| 分代 ZGC 和 G1 的分代有什么区别? | ① ZGC 分代是物理分页(Young/Old ZPage),G1 是逻辑 Region 标记 ② ZGC 分代基于指针颜色实现年轻代快速回收,G1 基于 RSet 跨代引用 ③ ZGC 分代后停顿仍 <1ms,G1 年轻代 GC 仍 STW |
面试回答模板(30 秒版)
延伸准备(加分项)
- G1 的 RSet 与 ZGC 的指针自愈对比:深入讲清为什么 RSet 是 G1 的瓶颈(跨代引用扫描成本),而 ZGC 用指针状态位避免了所有跨代引用记录——这是两者设计哲学的根本差异。
- ZGC 的分代实现细节:JDK 21 分代 ZGC 如何用两个 Marked 位实现年轻代/老年代并发标记,以及为什么分代后吞吐提升约 20%~30%。
- 实际压测方法:用
-Xlog:gc*分析 G1 的 Evacuation Pause 和 ZGC 的 Concurrent Mark/Transfer 阶段耗时,对比同堆大小下的 P99 延迟曲线——面试中能引用具体数据(如 64GB 堆,G1 P99 35ms vs ZGC P99 1.2ms)会显著加分。
想系统备战大厂大模型/Agent 开发?NiceOffer 提供 SDE+LLM 双轨 1v1 陪跑,合同保底 40w 年薪,文末扫码咨询。