大家好,我是五阳。
GC 话题始终霸占面试必问排行榜,很多人对 GC 原理了然于胸,但是苦于没有实践经验,因此本篇文章将分享我的GC 优化实践。一个很小的优化,产生了非常好的效果。
现在五阳将优化过程给大家汇报一下。
一、背景
我所负责的 A 服务每天的凌晨会定时执行一个批量任务,每天执行时都会触发 GC 频率告警,偶尔单机 CPU 负载超过 60%时,会触发 CPU 高负载告警。
曾经有考虑过通过单机限流器,限制任务执行速率,从而降低机器负载。然而因为业务上希望定时任务尽快执行完,所以优化方向就放在了如何降低 CPU 负载,如何降低 GC 频率。
1.1 配置和负载
- 版本:java8
- GC 回收器:ParNew + CMS
- 硬件:8 核 16G 内存,Centos6.8
- 高峰期CPU 平均负载(分钟)超过 50%(每个公司计算口径可能不同。我司的历史经验超过 70%后,接口性能将会快速恶化)
1.2 优化前的 GC情况
不容乐观。
- 高峰期 Young GC频率 70次/min,单次 ygc 平均时间 125ms;
- 高峰期 Full GC频率 每 3 分钟 1 次;单次 fgc 平均时间 610ms。
1.3 GC 参数和 JVM 配置
参数配置 | 说明 |
---|---|
-Xmx6g -Xms6g | 堆内存大小为6G |
-XX:NewRatio=4 | 老年代的大小是新生代的 4 倍,即老年代占4.8G,新生代占1.2G |
-XX:SurvivorRatio=8 | Eden:From:To= 8:1:1,即Eden区占0.96G,两个Survivor区分别占0.12G |
-XX:ParallelCMSThreads=4 | 设置 CMS 垃圾回收器使用的并行线程数为 4 |
XX:CMSInitiatingOccupancyFraction=72 | 设置老年代使用率达到 72% 时触发 CMS 垃圾回收。 |
-XX:+UseParNewGC | 启用 ParNew 作为年轻代垃圾回收器 |
-XX:+UseConcMarkSweepGC | 启用 CMS 垃圾回收器 |
二、问题分析
2.1 增加 GC打印参数
由于打印GC信息不足,无法分析问题。因此添加了 以下GC 打印参数,以提供更多的信息
js
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintCommandLineFlags
-XX:+PrintGCDateStamps
-XX:+PrintHeapAtGC
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintReferenceGC
2.2 提前晋升现象
配置如上参数后,每次发生 younggc后,都会打印详细的 younggc 日志。通过分析 gc 日志,我发现日志中经常出现类似内容。 Desired survivor size 61054720 bytes, new threshold 2 (max 15)
new threshold是新的晋升阈值,是指对象在新生代经过 new threshold
轮 younggc后,就能晋升到老年代,这个值通过 MaxTenuringThreshold配置,默认值是 15,在原有理解中阈值是固定值 15,实际上这个值会动态调整。
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
Desired survivor 一般是 Survivor 区的一半。假设年龄 1至N 的对象大小,超过了 Desired size,那么下一次 GC 的晋升阈值就会调整为 N。举个例子,假设 age=1的对象为 80M,超过了 61M,那么下一次GC 的晋升阈值就是 1,所有超过 1 的对象都会晋升到老年代,无需等到年龄到 15。
如何分析 younggc 日志,可以参考我的另一篇文章。2024面试必问:系统频繁Full GC,你有哪些优化思路?第一步分析gc日志
2.3 老年代增长速度过快
为了印证是否发生提前晋升,我通过监控查看到在事发时间,老年代内存的涨幅和 Survivor的内存基本一致,看来新生代的对象确实提前晋升到老年代了。
grep 分析历次 GC 后的晋升阈值后,我发现绝大部分情况下,新生的对象无法在 15 次 GC后进入到老年代,基本上三次以后就会提前晋升到老年代...... 这解释了为什么会发生频繁的 FullGC。
假设每次提前晋升 100M 到老年代,每分钟超过 15 次 ygc,则每分钟将会有 1.5G 对象进入老年代。
因为频繁地提前晋升,老年代的增长速度极快。 在高峰期时,往往 2 至 3 分钟左右,老年代内存就会触达 72% 的阈值,从而发生 FullGC。
2.4 新生代内存不足
即便老年代配置 4.8G 的大内存,但频繁地发生提前晋升,老年代也很快被打满。这背后的根本原因在于 新生代的内存太小了。 新生代,总共 1.2G 大小,Survivor才 120M,这远远不够。
于是我们调整了内存分配。调整后如下
- -Xmx10g -Xms10g -Xmn6g
- -XX:SurvivorRatio=8
- 堆内存由 6G 增加到 10G
- 大部分堆内存(6G)分配给新生代。新生代内存从 1.2G 增加到 6G。
- Eden:From:To 的比例依然是 8:1:1
- Eden大小从 0.96 G 增加到 4.8 G。
- Survivor区由 120 M 增加到 600 M。
三、优化效果
虽然改动不大,但是优化效果十分显著。由于公司监控有水印,我无法截图取证,敬请谅解。
3.1 GC频率明显下降
- 高峰期 ygc 70 次/min 降到了 12 次/min,下降幅度达83%(单机 500 QPS)
- 高峰期 fgc 三分钟1 次 ,降到了 每天 1 次 Full GC。
- younggc 和 fullgc 单次平均耗时保持不变。
3.2 CPU 负载降低 30%+
- 优化之前高峰期 cpu 平均负载超过 50%;优化后降到了不足 30%,高峰期负载下降了 40%。
- CPU负载每日平均值 由 29%,降到了 20%。日平均负载下降了 32%。
3.3 核心接口性能显著提升
核心接口耗时下降明显
- 接口 A 高峰期 TPS 100/秒,tp999 由 200毫秒 降到了 150 毫秒, tp9999 由 400 毫秒降到了 300 毫秒,接口耗时下降超过 25%!
- 接口 B 高峰期QPS 250/秒, tp999 由 190 毫秒降到了 120 毫秒, tp9999 由 450 毫秒下降到了 150 毫秒,接口耗时下降分别下降 37%和 67%!
- 接口 B 低峰期降幅更加明显,tp999 由 80 毫秒降到了 10 毫秒,下降幅度接近 90%!
后来又适当微调了 JVM 内存分配比例,但是优化效果不明显。
四、总结
经过此次 GC 优化经历,我学到了如下经验
- 要通过 GC 日志分析 GC 问题。
- 调整JVM 内存,保证足够的新生代内存。
- 优化 GC 可以降低接口耗时,提高接口可用性。
- 优化 GC 可以有效降低机器 CPU 负载,提高硬件使用率。
反过来当接口性能差、cpu负载高的时候,不妨分析一下 GC ,看看有没有优化空间。
详细了解如何分析 younggc 日志,可以参考我的另一篇文章。2024面试必问:系统频繁Full GC,你有哪些优化思路?第一步分析gc日志
关注五阳~ 了解更多我在大厂的实际经历