4. GC策略与调优:常见GC算法对比、CMS与G1实战参数、ZGC入门
GC调优,说白了就是跟JVM的垃圾回收器打交道。很多同学觉得这玩意儿玄学,其实不然。我做了这么多年运维,踩过的坑不少,但GC这块只要理清思路,大部分问题都能找到规律。
今天咱们就聊聊GC策略。我会把常见的算法、CMS和G1的实战参数,还有ZGC的入门知识,一次性讲清楚。
4.1 常见GC算法对比
先看基础。GC算法就那么几种,但每种都有它的脾气。
| 算法 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 标记-清除 | 先标记存活对象,再清除未标记的 | 实现简单,不移动对象 | 产生内存碎片,效率随对象增多下降 | 老年代,配合CMS使用 |
| 标记-复制 | 将存活对象复制到另一块区域 | 无碎片,分配高效 | 浪费一半内存,存活率高时效率低 | 新生代(Young GC) |
| 标记-整理 | 标记存活对象,然后向一端移动 | 无碎片,内存利用率高 | 移动对象开销大,STW时间长 | 老年代,CMS的备选方案 |
| 分代收集 | 按对象存活周期分代,不同代用不同算法 | 综合性能最优,兼顾吞吐和延迟 | 实现复杂,跨代引用处理麻烦 | 几乎所有生产环境 |
我个人习惯把分代收集当作默认选择。你想想看,新生代对象朝生夕死,用复制算法最合适;老年代对象活得久,用标记-整理或标记-清除更合理。这就是为什么HotSpot虚拟机一直坚持分代设计。
核心观点:没有完美的GC算法,只有最适合当前场景的算法。选型时优先考虑延迟要求,其次才是吞吐量。
4.2 CMS实战参数
CMS(Concurrent Mark Sweep)是老牌的低延迟GC。它的目标是减少STW时间,适合对响应时间敏感的应用。
我在项目中遇到过一个问题:一个电商平台的订单服务,用了CMS,但每次GC后CPU飙升。后来发现是CMS的并发模式失败(Concurrent Mode Failure)导致的。嗯,这里要注意,CMS最怕的就是这个。
4.2.1 核心参数
-XX:+UseConcMarkSweepGC # 启用CMS
-XX:+UseParNewGC # 新生代使用ParNew(CMS的标配)
-XX:CMSInitiatingOccupancyFraction=70 # 老年代使用率达到70%时触发CMS
-XX:+UseCMSInitiatingOccupancyOnly # 只使用设定的阈值,不动态调整
-XX:+CMSScavengeBeforeRemark # 在重新标记前做一次Young GC
-XX:+CMSParallelRemarkEnabled # 并行重新标记
-XX:CMSFullGCsBeforeCompaction=5 # 每5次Full GC后做一次整理
-XX:+UseCMSCompactAtFullCollection # Full GC时进行内存整理
我建议你把 -XX:CMSInitiatingOccupancyFraction 设到70%左右。设得太低,CMS频繁触发,浪费CPU;设得太高,容易触发Concurrent Mode Failure,导致Full GC。
避坑指南:我曾经把CMSInitiatingOccupancyFraction设到90%,结果线上频繁出现Concurrent Mode Failure。后来改成75%,配合CMSScavengeBeforeRemark,问题解决了。记住,CMS的并发收集阶段不能回收浮动垃圾,所以阈值不能设得太高。
4.2.2 CMS的六个阶段
- 初始标记(STW):标记GC Roots直接关联的对象。时间很短。
- 并发标记:从GC Roots开始遍历整个对象图。耗时最长,但与应用并发执行。
- 并发预清理:处理并发标记阶段发生变化的对象。
- 重新标记(STW):修正并发标记期间变动的对象。时间比初始标记长,但比并发标记短。
- 并发清除:清除未标记的对象。与应用并发执行。
- 并发重置:重置CMS内部数据结构,准备下一次GC。
你看,CMS只有两个STW阶段:初始标记和重新标记。这就是它低延迟的秘密。但代价是CPU占用高,而且会产生内存碎片。
4.3 G1实战参数
G1(Garbage First)是CMS的接班人。它把堆分成多个Region,优先回收垃圾最多的Region。说白了,就是“哪块垃圾多,先收哪块”。
我记得有一次帮一个金融客户调优,他们的交易系统用了G1,但延迟总是不稳定。后来发现是Region大小设置不合理。嗯,这里要重点讲一下。
4.3.1 核心参数
-XX:+UseG1GC # 启用G1
-XX:G1HeapRegionSize=4m # Region大小(1MB~32MB,默认根据堆大小自动计算)
-XX:MaxGCPauseMillis=200 # 目标最大停顿时间(默认200ms)
-XX:G1NewSizePercent=5 # 新生代初始占比(默认5%)
-XX:G1MaxNewSizePercent=60 # 新生代最大占比(默认60%)
-XX:G1HeapWastePercent=5 # 允许浪费的堆空间百分比(默认5%)
-XX:G1MixedGCLiveThresholdPercent=85 # 混合GC中存活对象占比阈值
-XX:G1MixedGCCountTarget=8 # 混合GC的目标次数
-XX:G1ReservePercent=10 # 预留空间百分比,防止晋升失败
注意:G1的停顿时间目标(MaxGCPauseMillis)不是硬性保证。如果设得太小(比如10ms),G1会频繁调整,反而影响性能。我建议从200ms开始,根据实际监控逐步下调。
4.3.2 G1的Region设计
G1把堆分成大小相等的Region。每个Region可以是Eden、Survivor、Old或者Humongous(大对象区域)。这种设计让G1可以灵活地调整各代大小,不需要固定比例。
你想想看,传统分代GC的年轻代和老年代是连续的,大小固定。G1打破了这种限制,每个Region独立管理,回收时只选垃圾最多的Region,效率自然高。
4.3.3 实战调优建议
- Region大小:建议设为2MB~8MB。太小会导致Region数量过多,管理开销大;太大则失去细粒度优势。
- 停顿时间:从200ms开始,观察GC日志。如果实际停顿远低于目标,可以适当调低;如果频繁超时,需要调高。
- 混合GC:G1的混合GC会回收老年代Region。如果老年代增长快,可以调大G1MixedGCLiveThresholdPercent,让更多Region参与回收。
个人经验:我曾经遇到一个案例,G1的Full GC频繁触发。排查后发现是Humongous对象(超过Region大小50%的对象)导致的。解决方案是调大Region大小,或者优化代码减少大对象分配。记住,G1处理大对象很吃力。
4.4 ZGC入门
ZGC(Z Garbage Collector)是JDK 11引入的低延迟GC。它的目标是STW时间不超过10ms,而且不随堆大小增长。说白了,就是“堆越大,越能体现ZGC的价值”。
我刚开始接触ZGC时,觉得它太激进了。但实际测试下来,确实惊艳。嗯,这里简单介绍一下。
4.4.1 核心特性
- 染色指针:ZGC在指针中存储元数据,不需要额外的内存来记录对象状态。
- 读屏障:在读取对象时检查指针状态,实现并发标记和重定位。
- 并发整理:ZGC的整理阶段是并发的,不需要STW。
- Region动态调整:ZGC的Region大小动态变化,适应不同大小的对象。
4.4.2 启用参数
-XX:+UseZGC # 启用ZGC(JDK 11+)
-XX:ZAllocationSpikeTolerance=2.0 # 分配突发容忍度(默认2.0)
-XX:ZCollectionInterval=120 # 强制GC间隔(秒)
-XX:ZFragmentationLimit=25 # 碎片率限制(默认25%)
-XX:ZMarkStackSpaceLimit=16MB # 标记栈空间限制
-XX:ZUncommitDelay=300 # 内存归还延迟(秒)
适用场景:ZGC适合大堆(100GB+)、低延迟(10ms以内)的应用。如果你的堆小于4GB,或者对吞吐量要求极高,ZGC可能不是最佳选择。
4.4.3 ZGC的局限
ZGC虽然好,但不是万能的。我在测试中发现几个问题:
- CPU占用高:ZGC的读屏障和并发标记会消耗大量CPU。在CPU资源紧张的环境下,可能影响业务。
- 内存占用高:ZGC需要额外的内存来支持染色指针和并发整理。实际内存占用比G1高10%~20%。
- JDK版本限制:ZGC在JDK 11是实验特性,JDK 15才正式发布。建议使用JDK 17+。
4.5 GC选型决策树
说了这么多,到底怎么选?我画了一张图,帮你理清思路。
4.6 实战调优步骤
最后,我总结一套通用的GC调优步骤。这套方法我在多个项目中验证过,效果不错。
- 明确目标:先问自己,要低延迟还是高吞吐?延迟要求是多少毫秒?
- 选择GC:根据堆大小和延迟要求,用上面的决策树选一个。
- 设置基础参数:堆大小、Region大小、停顿时间目标等。
- 运行并监控:用GC日志、JMX、Prometheus等工具收集数据。
- 分析瓶颈:看GC频率、STW时间、晋升情况、碎片率等。
- 调整参数:每次只改一个参数,观察效果。
- 验证结果:确认是否达到目标。如果没达到,回到第4步。
重要提醒:GC调优不是一劳永逸的。业务变了、流量变了、硬件变了,GC参数都要跟着调整。我建议每季度做一次GC健康检查,确保系统始终处于最佳状态。