Java 垃圾回收算法:G1与ZGC的停顿时间对比
高并发、低延迟是现代Java应用的核心诉求。在众多垃圾回收器(GC)中,G1(Garbage-First)长期作为JDK 8以来的默认选择,而ZGC(Z Garbage Collector)则是后来居上的低延迟新星。深入理解两者在停顿时间上的底层差异,是进行JVM调优的关键。
1. G1 的停顿机制:基于“复制”的权衡
G1 将堆内存划分为多个大小相等的独立区域。它通过追踪每个区域的垃圾积压程度,优先回收垃圾最多的区域,这也是“Garbage-First”名字的由来。
核心逻辑:G1 的垃圾回收过程主要包括“标记”和“复制”两个阶段。为了整理碎片,G1 必须将存活对象从一个 Region 复制到另一个 Region。这个过程涉及内存指针的修改,必须暂停所有应用线程,即产生“Stop-The-World”(STW)。
以下流程描述了 G1 在面对高分配速率时的处理过程:
停顿时间特征:
G1 允许用户设定预期的最大停顿时间目标(例如 -XX:MaxGCPauseMillis=200)。JVM 会根据历史数据,计算出在规定时间内能回收多少个 Region,从而控制停顿。
局限性:当对象存活率很高(即需要复制大量对象)或者内存碎片严重时,为了达到回收目标,G1 可能被迫进行更长时间的 STW,甚至退化为 Serial GC 进行全量压缩,导致停顿时间剧烈抖动。
2. ZGC 的停顿机制:基于“染色指针”的并发
ZGC 的设计目标是将停顿时间控制在 10 毫秒以内(JDK 17 以后甚至能达到亚毫秒级)。它通过彻底的并发化设计,几乎消除了将堆内存冻结进行对象移动的必要。
核心技术:ZGC 引入了读屏障和染色指针。
- 染色指针:利用 64 位操作系统中指针的高位几位(Linux/x64-64 使用第 42-47 位)来存储对象的状态(如 Finalizable、Remapped、Moved 等)。
- 多重映射:将同一块物理内存映射到虚拟内存的三个不同地址视图,从而在应用线程访问对象时,通过读屏障自动修正指针,无需等待 GC 线程完成所有更新。
停顿时间特征:
ZGC 的停顿仅发生在 GC 的初始标记和重新标记阶段,这两个阶段只需要遍历 GC Roots(如线程栈、静态变量),不涉及对象移动或大规模堆扫描。
其停顿时间公式近似为:
$$T_{ZGC\_Pause} \propto \text{GC\_Roots\_Count}$$
这意味着,无论堆内存是 4GB 还是 4TB,只要 GC Roots 的数量保持稳定,ZGC 的停顿时间就几乎恒定。
3. 核心指标对比
为了更直观地展示两者在面对不同场景下的表现,以下是关键维度的对比。
| 维度 | G1 GC | ZGC |
|---|---|---|
| 设计目标 | 平衡吞吐量与延迟 | 极致低延迟,停顿 < 10ms |
| 停顿时间稳定性 | 波动较大,随存活数据量增加而增加 | 极度稳定,不随堆大小增加而增加 |
| 堆内存上限 | 通常建议不超过 32GB - 64GB | 支持 16TB (仅受限于硬件) |
| 对象移动 | STW 期间物理复制 | 并发重定位,利用读屏障修正指针 |
| CPU 开销 | 较低 | 较高(读屏障带来的额外 CPU 消耗) |
| 适用场景 | 通用型服务器应用,对吞吐量有要求 | 超大堆内存、对延迟极其敏感的系统 |
4. 实战:如何从 G1 切换到 ZGC
如果你的应用受困于 G1 的偶发长停顿,且运行在 JDK 11 或更高版本(建议 JDK 17+),可以按照以下步骤尝试切换到 ZGC。
4.1 验证运行环境
检查当前 JDK 版本。ZGC 在 JDK 15 之前是实验性质的,需要解锁实验特性。
在终端执行以下命令:
java -version
如果版本低于 11,必须先升级 JDK。
4.2 调整启动参数
移除 G1 相关的配置参数,如 -XX:+UseG1GC, -XX:MaxGCPauseMillis, -XX:ParallelGCThreads 等。
添加 ZGC 的开启参数。ZGC 的配置极其简单,通常只需开启开关即可,它会自动调整。
修改启动脚本(如 setenv.sh 或 application.conf):
# 开启 ZGC
-XX:+UseZGC
4.3 设置堆大小与日志
设置初始堆大小 (-Xms) 和最大堆大小 (-Xmx)。为了避免动态调整堆大小带来的性能损耗,建议将两者设为相同值。
-Xms8g -Xmx8g
开启 GC 日志以监控效果。ZGC 使用新的统一日志框架。
-Xlog:gc*:file=gc.log:time,uptime,level,tags
4.4 验证与微调
启动应用并观察 gc.log。重点查看日志中的 Pause 相关行。
G1 日志示例(关注 Total 时间):
[GC pause (G1 Evacuation Pause) (young), 0.2301234 secs]
ZGC 日志示例(关注 Pause 和 GC 的分离):
[info] gc(start), GC(1) Pause Init Mark 0.452ms
[info] gc(start), GC(1) Concurrent Mark 15.203ms
对比停顿时间。如果发现停顿确实稳定在 10ms 以内,但 CPU 使用率显著上升,这是正常现象。如果 CPU 无法承受,可以考虑适当增加堆大小来降低 GC 频率,或者退回 G1 并通过 -XX:MaxGCPauseMillis 放宽延迟要求以换取吞吐量。

暂无评论,快来抢沙发吧!