3、GC选择与调优:Serial、Parallel、CMS、G1、ZGC的适用场景与核心参数
GC调优,说白了就是给JVM找个合适的“清洁工”。选错了,应用就卡;选对了,性能起飞。我见过太多团队一上来就上G1,结果小堆内存反而更慢。今天咱们把这五个主流垃圾收集器掰开揉碎,讲清楚它们各自该什么时候用、怎么配。
核心原则:没有最好的GC,只有最适合你场景的GC。吞吐量、延迟、内存占用,三者不可兼得。
3.1 Serial GC:单线程的“老实人”
Serial GC是最简单的收集器。它只用单线程做垃圾回收,回收时会暂停所有应用线程(Stop-The-World)。
适用场景:
- 堆内存 < 100MB的客户端应用
- 单核CPU环境
- 开发/测试环境快速验证
核心参数:
-XX:+UseSerialGC # 启用Serial GC
-XX:SurvivorRatio=8 # Eden:Survivor比例,默认8:1:1
-XX:NewRatio=2 # 新生代:老年代比例,默认1:2
我的经验:有一次在嵌入式设备上跑Java应用,堆只有64MB。团队想用Parallel GC,结果每次GC反而更慢。换成Serial后,吞吐量直接翻倍。小堆场景,别折腾。
3.2 Parallel GC:吞吐量优先的“多面手”
Parallel GC也叫吞吐量收集器。它用多线程并行回收,目标是最大化应用吞吐量。说白了,就是让CPU尽可能多地跑业务代码,少做GC。
适用场景:
- 批处理、数据分析、科学计算
- 后台任务、离线作业
- 对延迟不敏感、但对吞吐量有要求的系统
核心参数:
-XX:+UseParallelGC # 启用Parallel GC
-XX:+UseParallelOldGC # 老年代也使用并行回收(JDK8默认)
-XX:ParallelGCThreads=4 # 并行GC线程数,通常等于CPU核心数
-XX:MaxGCPauseMillis=100 # 期望最大停顿时间(毫秒)
-XX:GCTimeRatio=99 # 目标GC时间占总时间1%
注意:MaxGCPauseMillis只是一个期望值,不是硬性保证。Parallel GC会动态调整堆大小来尽量满足这个目标。设得太小反而可能导致频繁GC,降低吞吐量。
我个人习惯在批处理任务中这样配:
java -Xms4g -Xmx4g -XX:+UseParallelGC -XX:ParallelGCThreads=8 -XX:MaxGCPauseMillis=200 -jar batch-job.jar
3.3 CMS GC:低延迟的“老前辈”
CMS(Concurrent Mark Sweep)是第一款主打低延迟的收集器。它大部分工作与应用线程并发执行,只有初始标记和重新标记需要暂停。
嗯,这里要注意:CMS在JDK9开始被标记为废弃,JDK14正式移除。但很多老项目还在用,你不得不懂。
适用场景:
- Web服务、中间件、在线交易系统
- 堆内存2GB~4GB,延迟要求 < 100ms
- JDK8及更早版本
核心参数:
-XX:+UseConcMarkSweepGC # 启用CMS
-XX:+UseParNewGC # 新生代使用并行回收(CMS默认搭配)
-XX:CMSInitiatingOccupancyFraction=70 # 老年代使用率70%时触发CMS
-XX:+UseCMSInitiatingOccupancyOnly # 只使用上述比例触发,不自动调整
-XX:+ExplicitGCInvokesConcurrent # System.gc()触发并发GC而非Full GC
避坑指南:我曾经在一个电商平台上遇到CMS并发模式失败(Concurrent Mode Failure)。原因是老年代碎片太多,CMS无法完成并发回收,被迫降级为Serial Old做Full GC。那次停顿了8秒,线上直接报警。解决方案是调低CMSInitiatingOccupancyFraction到60%,给GC留更多空间。
3.4 G1 GC:区域化的大规模“调度员”
G1(Garbage First)把堆分成多个Region(区域),每次只回收垃圾最多的区域。它既能控制延迟,又能处理大堆。
从JDK9开始,G1成为默认收集器。为什么?因为它平衡了吞吐量和延迟,而且能处理4GB以上的堆。
适用场景:
- 堆内存4GB~64GB
- 延迟要求 < 200ms
- 需要可预测的GC停顿时间
核心参数:
-XX:+UseG1GC # 启用G1(JDK9+默认)
-XX:MaxGCPauseMillis=200 # 目标最大停顿时间(默认200ms)
-XX:G1HeapRegionSize=4m # Region大小(1MB~32MB)
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的堆占用率
-XX:G1NewSizePercent=5 # 新生代初始占比
-XX:G1MaxNewSizePercent=60 # 新生代最大占比
我的调优经验:G1的MaxGCPauseMillis不要设得太激进。我见过有人设成50ms,结果G1为了满足这个目标,每次只回收一点点Region,导致GC频率飙升,吞吐量反而下降。一般设150~300ms比较合理。
G1还有一个重要概念——Mixed GC。它不像CMS只回收老年代,而是同时回收新生代和部分老年代Region。这避免了CMS那种碎片化问题。
3.5 ZGC:亚毫秒级的“未来战士”
ZGC(Z Garbage Collector)是JDK11引入的实验性收集器,JDK15正式发布。它的核心卖点是:无论堆多大,停顿时间都控制在10ms以内。
为什么会这么强?因为它用了染色指针和读屏障技术,大部分工作都是并发的,连重标记阶段都几乎不停顿。
适用场景:
- 超大堆内存(64GB~16TB)
- 延迟要求 < 10ms
- 金融交易、实时计算、在线游戏
核心参数:
-XX:+UseZGC # 启用ZGC(JDK15+)
-XX:ZAllocationSpikeTolerance=2.0 # 分配波动容忍度
-XX:ZCollectionInterval=120 # 强制GC间隔(秒)
-XX:ZFragmentationLimit=25 # 允许的最大碎片率(%)
注意:ZGC目前不支持分代回收(JDK21开始支持实验性分代ZGC)。这意味着每次GC都会扫描整个堆,虽然停顿短,但CPU开销不小。如果你的应用CPU资源紧张,慎用。
3.6 核心参数对比表
| 收集器 | 核心参数 | 默认值 | 调优建议 |
|---|---|---|---|
| Serial | -XX:+UseSerialGC | 客户端默认 | 堆<100MB时使用 |
| Parallel | -XX:ParallelGCThreads | CPU核心数 | 批处理设为核心数,Web服务可减半 |
| CMS | -XX:CMSInitiatingOccupancyFraction | 70 | 老年代增长快则调低到50~60 |
| G1 | -XX:MaxGCPauseMillis | 200 | 设150~300,别低于100 |
| ZGC | -XX:+UseZGC | — | JDK17+推荐,注意CPU开销 |
3.7 如何选择?我的决策框架
每次做GC选型,我都会问自己三个问题:
- 堆多大? < 100MB用Serial,100MB~4GB看延迟要求,> 4GB直接上G1或ZGC。
- 延迟要求多高? < 10ms选ZGC,10~200ms选G1,> 200ms选Parallel。
- CPU资源够吗? 多核富余用并行收集器,单核或CPU紧张用Serial或CMS。
最后一句忠告:别在生产环境直接换GC。先在压测环境跑三天,看GC日志、吞吐量、延迟分布。我曾经见过一个团队上线前把Parallel换成G1,结果Full GC次数反而多了3倍。调优,永远是先测量,再调整。