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对算法的实现细节差异很大,参数调优的方向也完全不同。
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分析,比瞎猜参数靠谱一百倍。