3. 性能优化基础

GC调优,说白了就是给JVM找个合适的“清洁工”。选错了,应用就卡;选对了,性能起飞。我见过太多团队一上来就上G1,结果小堆内存反而更慢。今天咱们把这五个主流垃圾收集器掰开揉碎,讲清楚它们各自该什么时候用、怎么配。

核心原则:没有最好的GC,只有最适合你场景的GC。吞吐量、延迟、内存占用,三者不可兼得。

GC选择决策流程 应用启动 堆内存 < 100MB? 是 Serial GC 否 延迟敏感? 否 Parallel GC 是 堆 > 4GB? 否 CMS GC 是 延迟 < 10ms? 否 G1 GC 是 ZGC

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选型,我都会问自己三个问题:

  1. 堆多大? < 100MB用Serial,100MB~4GB看延迟要求,> 4GB直接上G1或ZGC。
  2. 延迟要求多高? < 10ms选ZGC,10~200ms选G1,> 200ms选Parallel。
  3. CPU资源够吗? 多核富余用并行收集器,单核或CPU紧张用Serial或CMS。

最后一句忠告:别在生产环境直接换GC。先在压测环境跑三天,看GC日志、吞吐量、延迟分布。我曾经见过一个团队上线前把Parallel换成G1,结果Full GC次数反而多了3倍。调优,永远是先测量,再调整。