2021-12-03 21:50 分左右出现请求量暴涨约 50 倍左右,业务方反馈接口响应变慢,甚至出现大量超时。
当时没有接入监控组件,通过 jstat -gc pid 命令查看 GC 情况,发现出现大量 Full GC,因此决定对服务进行 JVM 调优。
相关概念
Serial 收集器
- Serial:新生代,复制算法,暂停所有用户线程。
- Serial Old:老年代,标记-整理算法,暂停所有用户线程。
工作示意图:

ParNew 收集器
ParNew 收集器实质上是 Serial 收集器的多线程并行版本

Parallel 收集器
关注点:吞吐量优先
- Parallel Scavenge:新生代,复制算法,暂停所有用户线程。
- Parallel Old:老年代,标记-整理算法,暂停所有用户线程。
CMS 收集器
HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
关注点:最短回收停顿时间
老年代,基于标记-清除算法

其中:初始标记、重新标记这两个步骤仍然需要暂停所有用户线程。
G1 收集器
CMS 收集器的替代者,但 G1 收集器 内存占用(Footprint)、负载更高。
软实时垃圾收集器
关注点:建立可预测的停顿时间模型

组合

当前现状
当前服务的 JVM 配置参数如下:
1 | -XX:MetaspaceSize=1G -XX:MaxMetaspaceSize=1G -Xms2G -Xmx2G -Xss512k -XX:SurvivorRatio=8 -XX:NewRatio=2 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs |
从参数上分析,存在以下问题:
未显示指定收集器
JDK8 默认使用 Parallel 收集器,新生代采用 Parallel Scavenge,老年代采用 Parallel Old 进行垃圾收集。这套配置的特点是:吞吐量优先,一般适用于后台型的服务,例如:批量处理,科学计算等对吞吐量敏感,对延迟不敏感的场景。因此不适合 Parallel 收集器,应该选择更适合的收集器。
堆大小配置不合理
堆内存只分配了 2GB,机器剩余可用内存为 18 GB。
新生代配比不合理
当前服务主要提供 API,这类服务的特点是常驻对象会比较少,绝大多数对象的生命周期都比较短,经过一次或两次 Young GC 就会消亡。
堆内存为 2G(-Xms2G -Xmx2G),新生代老年代比例为 1:2(-XX:NewRatio=2),新生代约为 680MB,Eden 和 Survivor 的大小比例为 8:1 (-XX:SurvivorRatio=8),新生代可用容量为 680MB * 90% = 612 MB。
这意味着,当服务负载较高时,请求并发较大,年轻代中的 Eden 和 S0 区域会被迅速填满,导致 Young GC 比较频繁。除此之外,还可能会引起应被 Young GC 回收的对象进入老年代,增加 Full GC 的频率。由于老年代使用的是 Parallel Old,GC 时无法与用户线程并发执行,导致长时间停顿,可用性下降,响应时间上升。
优化方案
主流收集器组合有:
- Parallel Scavenge + Parallel Old:吞吐量优先,后台型服务适用。
- ParNew + CMS:经典的低停顿收集器,绝大多数商用、延时敏感的服务在使用。
- G1:堆内存比较大(6G-8G以上)的时候表现出比较高吞吐量和短暂的停顿时间.
- ZGC:JDK11 推出的低延迟垃圾回收器,目前处在实验阶段。
注意:
新生代太大,Young GC 的频率一定越小,但单次 Young GC 的时间旧越长,
老年代太小,稍微晋升一些对象就会触发 Full GC,得不偿失。
因此新生代和老年代的大小结合实际情况比较分析,最终获得最合适的配置。
方案一:
1 | java -XX:MetaspaceSize=8g -XX:MaxMetaspaceSize=8g -Xms8g -Xmx8g -Xss256k -XX:SurvivorRatio=8 -XX:NewRatio=2 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs |
方案二:
1 | java -Xms8G -Xmx8G -Xmn3G -XX:MetaspaceSize=8G -XX:MaxMetaspaceSize=8G -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSScavengeBeforeRemark -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs |
XX:+CMSScavengeBeforeRemark:在重新标记之前对年轻代做一次minor GC;
方案三:
1 | java -XX:MetaspaceSize=8g -XX:MaxMetaspaceSize=8g -Xms8g -Xmx8g -XX:+UseG1GC -XX:G1HeapRegionSize=8m -XX:MaxGCPauseMillis=180 -XX:InitiatingHeapOccupancyPercent=40 -XX:ConcGCThreads=8 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs - |
优化效果
GC 时间对比
方案一:

方案二:

方案三:

GC 频率对比
方案一:

方案二:

方案三:
