
在 Java 应用性能优化中,垃圾收集器的选择与调优是关键环节。随着应用规模的扩大和对响应时间要求的提高,传统的垃圾收集器(如 CMS)已难以满足现代应用的需求。G1 和 ZGC 作为现代垃圾收集器的代表,提供了更优的性能和更低的停顿时间。本文将深入解析 G1 和 ZGC 的工作原理、特性、参数优化及适用场景。
一、G1 垃圾收集器详解
1. 基本特性
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。其核心特点:
- 高概率满足停顿时间要求:以极高概率满足 GC 停顿时间要求
- 高吞吐量:同时具备高吞吐量性能特征
- Region 内存布局:将 Java 堆划分为多个大小相等的独立区域(Region)
- 最大 Region 数:JVM 最多可以有 2048 个 Region
- Region 大小:一般 Region 大小等于堆大小除以 2048,如堆大小为 4096M,则 Region 大小为 2M
2. G1 内存布局
G1 保留了年轻代和老年代的概念,但不再是物理隔阂,它们都是(可以不连续)Region 的集合:
- 默认年轻代占比:5%(堆大小为 4096M 时,年轻代约 200MB,对应约 100 个 Region)
- 年轻代最大占比 :60%(可通过
XX:G1MaxNewSizePercent调整) - Region 功能动态变化:一个 Region 可能之前是年轻代,回收后可能变成老年代
3. G1 工作原理
G1 垃圾收集器的运作过程分为四个阶段:
- 初始标记(Initial Mark,STW:暂停所有其他线程,记录 gc roots 直接能引用的对象,速度很快
- 并发标记(Concurrent Marking:遍历对象图做可达性分析,与 CMS 类似
- 最终标记(Remark,STW:重新标记,修正并发标记期间的变动
- 筛选回收(Cleanup,STW:根据用户期望的 GC 停顿时间制定回收计划
筛选回收阶段:G1 根据用户期望的 GC 停顿时间(-XX:MaxGCPauseMillis)制定回收计划,优先回收价值最大的 Region。例如,如果老年代有 1000 个 Region,但期望停顿时间只能回收 800 个,则只回收 800 个 Region。
4. G1 垃圾收集分类
| 类型 | 触发条件 | 说明 |
|---|---|---|
| YoungGC | Eden 区回收时间接近 -XX:MaxGCPauseMillis |
不是 Eden 区满就触发,G1 会计算回收时间 |
| MixedGC | 老年代占用率达到 -XX:InitiatingHeapOccupancyPercent(默认 45%) |
回收所有 Young 和部分 Old,避免 Full GC |
| Full GC | 无法满足 MixedGC 条件 | 采用单线程进行标记、清理和压缩整理 |
5. G1 关键参数
| 参数 | 默认值 | 说明 |
|---|---|---|
-XX:+UseG1GC |
- | 启用 G1 收集器 |
-XX:MaxGCPauseMillis |
200 | 目标暂停时间(毫秒) |
-XX:G1NewSizePercent |
5 | 新生代初始占比 |
-XX:G1MaxNewSizePercent |
60 | 新生代最大占比 |
-XX:InitiatingHeapOccupancyPercent |
45 | 老年代占用率达到此值触发 MixedGC |
-XX:G1MixedGCLiveThresholdPercent |
85 | Region 中存活对象低于此值才回收 |
-XX:G1MixedGCCountTarget |
8 | 一次回收过程中做筛选回收的次数 |
-XX:G1HeapWastePercent |
5 | GC 过程中空出来的 Region 是否充足阈值 |
6. G1 优化建议
核心原则:避免存活对象过多快速进入老年代,导致频繁触发 Mixed GC
- **合理设置
XX:MaxGCPauseMillis**:- 设置过低(如 20ms):可能导致每次回收的 Region 太少,收集速度跟不上分配速度
- 设置合理(100-300ms):平衡停顿时间和收集效率
- 避免对象过早进入老年代 :
- 调整
XX:MaxTenuringThreshold:减少对象晋升阈值 - 例如,将默认 15 改为 5,让对象需要经过 5 次 Minor GC 才进入老年代
- 调整
- 合理设置新生代大小 :
- 避免年轻代过大(如超过 60%),导致存活对象过多
- 例如,8G 内存的 JVM,设置
Xmn2G(2G 年轻代)
7. G1 适用场景
- 50% 以上的堆被存活对象占用
- 对象分配和晋升的速度变化非常大
- 垃圾回收时间特别长,超过 1 秒
- 8GB 以上的堆内存(建议值)
- 停顿时间要求 500ms 以内
典型案例:Kafka 等高并发消息系统,每秒处理几万甚至几十万消息,使用 G1 收集器,设置-XX:MaxGCPauseMillis=50ms,50ms 的停顿用户几乎无感知,系统可以一边处理业务一边收集垃圾。
二、ZGC 垃圾收集器详解
1. 基本特性
ZGC(Z Garbage Collector)是 JDK 11 中新加入的具有实验性质的低延迟垃圾收集器,其目标:
- 支持 TB 量级的堆:满足未来十年内所有 Java 应用的需求
- 最大 GC 停顿时间不超 10ms:比 CMS 更优
- 停顿时间不随堆增大而增长:几十 G 堆停顿时间 10ms 以下,几百 G 甚至上 T 堆也是 10ms 以下
- 不设分代(暂时):单代,没有分代概念
2. ZGC 内存布局
ZGC 的 Region 可以具有大、中、小三类容量:
| Region 类型 | 容量 | 用途 |
|---|---|---|
| 小型 Region | 2MB | 小于 256KB 的对象 |
| 中型 Region | 32MB | 256KB-4MB 的对象 |
| 大型 Region | 动态变化 | 4MB 或以上的大对象 |
大型 Region 特点:每个大型 Region 中只存放一个大对象,容量最小为 4MB,不会被重分配(因为复制大对象代价高)。
3. ZGC 核心技术
(1) 颜色指针(Colored Pointers)
ZGC 的核心设计之一,将 GC 信息保存在指针中,而非对象头:
- 64 位指针结构 :
- 18 位:预留给未来使用
- 1 位:Finalizable 标识
- 1 位:Remapped 标识
- 1 位:Marked1 标识
- 1 位:Marked0 标识
- 42 位:对象地址(支持 4T 内存)
为什么有 2 个 mark 标记:每个 GC 周期开始时,交换使用的标记位,使上次 GC 周期中修正的已标记状态失效。
(2) 读屏障(Read Barrier)
ZGC 采用读屏障,而不是写屏障:
- 工作原理:每次从堆中对象引用类型读取指针时,需要加上 Load Barriers
- 作用:当对象被移动时,读屏障会自动修正指针
读屏障示意图:
Object p = o // 无读屏障 o.doSomething() // 无读屏障 obj.fieldB = 1 // 无读屏障 Object p = o // 有读屏障 o.doSomething() // 有读屏障 obj.fieldB = 1 // 无读屏障
(3) NUMA-aware
ZGC 能自动感知 NUMA 架构并充分利用:
- NUMA:Non Uniform Memory Access Architecture,每个 CPU 对应一块内存
- 优势:提高内存访问效率,减少内存竞争
4. ZGC 工作过程
ZGC 的运作过程分为四个阶段:
- 并发标记(Concurrent Mark:遍历对象图做可达性分析,初始标记和最终标记有短暂 STW
- 并发预备重分配(Concurrent Prepare for Relocate:统计要清理的 Region,组成重分配集(Relocation Set)
- 并发重分配(Concurrent Relocate:将存活对象复制到新 Region,维护转发表(Forward Table)
- 并发重映射(Concurrent Remap:修正指向重分配集的引用
"自愈"能力:ZGC 的指针"自愈"能力,使用户线程访问重分配集中的对象时,自动转发到新对象。
5. ZGC 关键参数
| 参数 | 说明 |
|---|---|
-XX:+UnlockExperimentalVMOptions |
解锁实验性参数 |
-XX:+UseZGC |
启用 ZGC |
-XX:ZCollectionInterval |
定时触发 GC(默认不使用) |
-XX:ZProactive |
主动触发 GC(默认开启) |
-XX:ZPageSizePolicy |
设置 ZGC 的页大小策略 |
6. ZGC 触发时机
ZGC 有 4 种机制触发 GC:
- 定时触发 :默认不使用,可通过
ZCollectionInterval配置 - 预热触发:最多三次,堆内存达到 10%、20%、30% 时触发
- 分配速率:基于正态分布统计,计算内存 99.9% 可能的最大分配速率
- 主动触发:默认开启,堆内存增长 10% 或超过 5 分钟触发
7. ZGC 问题与挑战
- 浮动垃圾 :
- ZGC 的停顿时间在 10ms 以下,但执行时间远大于停顿时间
- 期间产生的新对象无法回收,只能等到下次 GC
- 没有分代 :
- 每次都需要进行全堆扫描
- 导致"朝生夕死"的对象未能及时回收
解决方案:增大堆容量,使程序得到更多喘息时间,但这是治标不治本的方案。
8. ZGC 适用场景
- 超大内存应用(几百 GB 以上)
- 低延迟要求(停顿时间 <10ms)
- 高吞吐量应用(ZGC 吞吐量降低 15% 可接受)
三、G1 与 ZGC 对比
| 特性 | G1 | ZGC |
|---|---|---|
| 目标 | 高吞吐 + 低停顿 | 低停顿(<10ms) |
| 停顿时间 | 可预测,可设置 | <10ms,与堆大小无关 |
| 内存模型 | Region 划分,分代 | Region 划分,单代 |
| 内存容量 | 适合 8GB-几百 GB | 适合几百 GB-上 TB |
| GC 算法 | 复制 + 标记整理 | 标记整理 |
| 并发性 | 部分并发(筛选回收阶段 STW) | 几乎完全并发 |
| 内存碎片 | 几乎无 | 无 |
| 适用场景 | 8GB 以上,停顿时间要求 500ms 内 | 超大内存,停顿时间要求 <10ms |
四、如何选择垃圾收集器
1. 选择原则
| 内存大小 | 推荐收集器 | 说明 |
|---|---|---|
| <100MB | Serial | 简单高效 |
| 单核,无停顿要求 | Serial | 无多线程开销 |
| 停顿时间 >1 秒 | Parallel | 高吞吐量 |
| 停顿时间 <1 秒 | CMS/G1 | 低停顿 |
| 4GB 以下 | Parallel | 简单高效 |
| 4-8GB | ParNew+CMS | 平衡停顿与吞吐 |
| 8GB 以上 | G1 | 适合大内存 |
| 几百 GB 以上 | ZGC | 低停顿,超大内存 |
2. 实际应用建议
- 中小型应用 :
- 8GB 以下:使用 G1(JDK 9+ 默认收集器)
- 8GB 以上:使用 G1
- 超大内存应用 :
- 100GB 以上:考虑 ZGC(JDK 11+)
- 500GB 以上:ZGC 是最佳选择
- 高并发低延迟系统 :
- 停顿时间 <50ms:G1
- 停顿时间 <10ms:ZGC
五、总结与建议
1. G1 核心优势
- 可预测的停顿时间 :通过
XX:MaxGCPauseMillis设置 - 空间整合:使用复制算法,几乎无内存碎片
- 适应大内存:适合 8GB 以上堆内存
- 混合收集:YoungGC 和 MixedGC 减少 Full GC
2. ZGC 核心优势
- 超低停顿:最大 GC 停顿时间 <10ms
- 与堆大小无关:几十 GB 和上 TB 堆的停顿时间相同
- 几乎完全并发:减少 STW 时间
- 支持超大内存:适合 TB 级堆内存
3. 优化建议
- G1 优化 :
- 设置合理的
XX:MaxGCPauseMillis(100-300ms) - 调整新生代大小,避免对象过早进入老年代
- 使用
XX:G1MixedGCCountTarget=8控制回收次数
- 设置合理的
- ZGC 优化 :
- 增大堆容量,减少浮动垃圾
- 启用
XX:ZProactive,避免频繁触发 Full GC - 监控 GC 日志,确认停顿时间是否符合预期
"G1 和 ZGC 代表了垃圾收集器的最新发展方向。G1 适合大多数大内存应用,ZGC 则为超大内存、超低延迟场景提供了最佳解决方案。理解它们的工作原理,能让你在 Java 应用性能优化的道路上走得更远。"
实战建议清单
| 问题类型 | 诊断方法 | 解决方案 |
|---|---|---|
| Full GC 频繁 | GC 日志分析 | 优化 G1 参数,增加 MixedGC 触发阈值 |
| 停顿时间长 | 监控停顿时间 | 选择 G1 或 ZGC,设置合理的停顿目标 |
| 大对象处理 | 分析大对象大小 | 设置 -XX:PretenureSizeThreshold,避免大对象进入老年代 |
| 超大内存应用 | 内存大小分析 | 选择 ZGC,设置 -XX:+UseZGC |
最后提醒:在实施 G1 或 ZGC 优化前,务必在测试环境验证效果。一个错误的 JVM 参数可能导致生产环境严重问题,而正确的优化能带来 10 倍性能提升。
"当你能读懂 G1 的工作原理、理解 ZGC 的创新设计、掌握优化技巧,你就真正掌握了 Java 应用的垃圾回收。从源码到执行,这是一条充满智慧的道路。"