3、GC参数详解:常见GC算法介绍、串行GC参数、并行GC参数、CMS GC参数、G1 GC参数

聊到JVM调优,GC这块是绕不开的硬骨头。我刚开始接触Java时,觉得GC就是个黑盒子——反正对象不用了它自己会回收,管那么多干嘛?直到线上一次Full GC把服务卡了十几秒,用户投诉电话打爆了,我才老老实实把GC参数从头啃了一遍。

说白了,GC调优就是跟垃圾回收的「停顿时间」和「吞吐量」较劲。没有完美的GC,只有最适合你业务的GC。今天我把五种主流GC掰开揉碎讲清楚,包括它们的核心参数和我在实战中踩过的坑。

核心观点:GC选型没有银弹。响应时间敏感的业务选CMS或G1,吞吐量优先的场景选Parallel GC,单机小内存用Serial GC反而更稳。

3.1 常见GC算法介绍

先快速过一下GC算法的底层逻辑。你想想看,JVM里的垃圾回收无非就三种思路:

  • 标记-清除(Mark-Sweep):先标记存活对象,再清除没标记的。缺点是会产生内存碎片,大对象分配容易触发Full GC。
  • 标记-复制(Mark-Copy):把内存分成两块,只用一块,回收时把存活对象复制到另一块。效率高,但内存利用率只有50%。
  • 标记-整理(Mark-Compact):标记存活对象后,把它们往一端移动,然后清理边界外的内存。解决了碎片问题,但移动对象有开销。

实际产品中的GC,都是这些算法的组合拳。比如新生代用复制算法(因为朝生夕死的对象多),老年代用标记-整理或标记-清除。嗯,这里要注意——不同GC对算法的实现细节差异很大,参数调优的方向也完全不同。

常见GC算法与适用场景 标记-清除 标记-复制 标记-整理 特点:实现简单,有碎片 适用:老年代(CMS) 缺点:碎片导致频繁Full GC 特点:效率高,浪费空间 适用:新生代(所有GC) 缺点:内存利用率50% 特点:无碎片,有移动开销 适用:老年代(Parallel/G1) 缺点:STW时间较长 实际GC = 多种算法组合 新生代复制 + 老年代整理/清除

3.2 串行GC参数(Serial GC)

串行GC是最古老的实现,单线程回收,回收期间会暂停所有应用线程(Stop-The-World)。听起来很粗暴对吧?但它在单核CPU、小内存(<100MB)的场景下,效率反而最高。

启用方式很简单:

-XX:+UseSerialGC

核心参数就这几个:

参数 说明 我的建议
-XX:SurvivorRatio Eden与Survivor比例,默认8:1 对象存活率低可调大Eden
-XX:PretenureSizeThreshold 大对象直接进入老年代的阈值 设成3MB左右,避免新生代复制开销
-XX:+UseSerialGC 启用串行GC 客户端模式默认就是它

实战经验:我曾经在一个嵌入式Java项目里用过Serial GC,堆内存只有64MB。当时试过Parallel GC,结果GC线程竞争CPU反而更慢。换回Serial GC后,吞吐量提升了15%。说白了,资源越少,越不要折腾。

3.3 并行GC参数(Parallel GC)

并行GC也叫吞吐量优先GC。它用多线程并行回收,目标是最大化应用吞吐量。JDK 8的默认GC就是它。

启用方式:

-XX:+UseParallelGC
-XX:+UseParallelOldGC   // 老年代并行回收,JDK 8默认已开启

调优参数我列个表:

参数 默认值 说明
-XX:ParallelGCThreads CPU核心数 并行回收的线程数
-XX:MaxGCPauseMillis 无上限 期望最大停顿时间(毫秒)
-XX:GCTimeRatio 99 GC时间占比目标,默认1%
-XX:+UseAdaptiveSizePolicy true 自适应调整堆大小

注意:Parallel GC的MaxGCPauseMillis只是个「期望值」,JVM会尽力满足,但不会强制。我曾经设成100ms,结果JVM把堆缩得很小,导致频繁GC,吞吐量反而暴跌。后来我设成200ms,配合GCTimeRatio=95(允许5%时间做GC),效果好了很多。

我个人习惯在批处理、离线计算这类场景用Parallel GC。比如一个数据清洗任务跑4小时,GC停顿多几秒无所谓,但吞吐量必须拉满。

3.4 CMS GC参数

CMS(Concurrent Mark Sweep)是第一款并发GC。它的核心思路是:尽量与应用线程并发执行,减少停顿时间。但代价是CPU占用高、有内存碎片。

启用方式:

-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC        // 新生代并行回收,CMS默认搭配

关键参数:

参数 说明
-XX:CMSInitiatingOccupancyFraction 触发CMS GC的老年代占比,默认68%
-XX:+UseCMSInitiatingOccupancyOnly 只用上述阈值触发,不依赖JVM动态计算
-XX:+CMSScavengeBeforeRemark 重新标记前先做一次Young GC,减少扫描范围
-XX:+CMSClassUnloadingEnabled 允许回收元空间中的类
-XX:ConcGCThreads 并发线程数,默认=(ParallelGCThreads+3)/4

避坑指南:我曾经遇到一个CMS「并发模式失败」(Concurrent Mode Failure)的问题。原因是老年代增长太快,CMS还没回收完,老年代就满了,JVM只能退化成Serial Old做Full GC。那次停顿了8秒多。解决方案是把CMSInitiatingOccupancyFraction从68%降到50%,给GC留出缓冲空间。

CMS在JDK 9后被标记为废弃,JDK 14正式移除。但很多老项目还在用,如果你维护的是JDK 8系统,CMS依然是低延迟场景的好选择。

3.5 G1 GC参数

G1(Garbage First)是JDK 9+的默认GC。它把堆分成多个Region(默认2048个),优先回收垃圾最多的Region。说白了,G1的目标是「在可控停顿时间内,尽可能多回收垃圾」。

启用方式:

-XX:+UseG1GC

核心参数:

参数 默认值 说明
-XX:MaxGCPauseMillis 200ms 目标最大停顿时间
-XX:G1HeapRegionSize 堆/2048 每个Region大小(1MB~32MB)
-XX:G1NewSizePercent 5% 新生代初始占比
-XX:G1MaxNewSizePercent 60% 新生代最大占比
-XX:InitiatingHeapOccupancyPercent 45% 触发并发标记的堆占用阈值
-XX:G1MixedGCLiveThresholdPercent 85% 混合回收时Region存活对象阈值

调优心得:G1的MaxGCPauseMillis别设太极端。我见过有人设成50ms,结果G1为了满足这个目标,每次只回收一点点Region,导致GC频率飙升,CPU全耗在GC上了。我个人习惯设成150~250ms,配合堆大小16GB以上,效果比较稳。

G1还有一个重要概念叫「混合回收」(Mixed GC)。它会在并发标记后,同时回收新生代和部分老年代Region。参数G1MixedGCCountTarget控制混合回收的次数,默认8次。如果觉得GC停顿太频繁,可以调大这个值,让每次混合回收多干点活。

嗯,最后说一句。GC调优没有银弹,最好的方式是用压测工具模拟真实流量,配合GC日志分析。我每次调优都会先加这几个参数:

-Xloggc:/path/to/gc.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintHeapAtGC

拿到日志后,用GCeasy或GCEasy.io分析,比瞎猜参数靠谱一百倍。

可可精品资料库