概述
在发现XXX系统的负载过高后确定解决方案,本文记录了整个过程。先说结论:
- jdk 1.8 中使用 CMS 收集器,UseAdaptiveSizePolicy 参数会被设置为 false,导致 young 区和 old 区大小不会动态调整
- jdk 1.8 中使用 CMS 收集器,默认的 newRatio=2 不会生效,需要显示配置此参数或者配置 young 大小。否则按照 cpu 核心数量计算 young 大小:64M * cpu 核心数 * 13 / 10
- 批量任务每次任务量过大,短时间内创建大量对象,导致 jvm 疯狂的 young gc
- 频繁 young gc 导致 CPU 使用率过高,系统
一、现象
在报警群里看到 XXX 服务所在的服务器负载很高, 4 核 16G 的配置,CPU 使用率 >90%
二、排查过程
查看 GC 情况
1.幸存区使用率接近 100%
2.频繁 young gc,每秒钟都有
使用 arthas 查看 CPU 占用情况
1.定时拉取任务占用了 95% 的 CPU
2.新生代大小 332MB
初步判断为新生代太小,而定时任务创建大量对象而且任务有堆积,对象不能被释放,从而导致幸存区使用率过高,发生频繁的 gc。
为什么新生代是 332.8MB
在做出调整之前要找到 newRatio 没生效的原因,为什么 8G 的堆内存,新生代只有 332MB
登上服务器查看服务启动时的参数配置:
java -server -Xmx2048m -Xms2048m -XX:+UseConcMarkSweepGC -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses -XX:+CMSScavengeBeforeRemark -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/admin/logs/tracking-center -Xloggc:/home/admin/logs/tracking-center/gc.log -Xmx8192m -Xms8192m ...
使用 CMS 收集器,设置了堆内存为 8G
查看 JVM 运行时参数
Heap Configuration: MinHeapFreeRatio = 40 MaxHeapFreeRatio = 70 MaxHeapSize = 8589934592 (8192.0MB) NewSize = 348913664 (332.75MB) MaxNewSize = 348913664 (332.75MB) OldSize = 8241020928 (7859.25MB) NewRatio = 2 SurvivorRatio = 8 MetaspaceSize = 21807104 (20.796875MB) CompressedClassSpaceSize = 1073741824 (1024.0MB) MaxMetaspaceSize = 17592186044415 MB G1HeapRegionSize = 0 (0.0MB)
young 大小 332.75MB
那么问题来了:
Q:JVM 的 newRatio 参数默认为 2,按这个配置,新生代应该为 8G*1/3, 差别太大了
A:JVM 有动态调整新生代和老年代大小的机制,1.8 中默认是自动开启的
Q:这么智能,怎么会在新生代最需要内存的时候只给分了 332MB
去另一台服务上确认一下配置,发现相同的启动参数,新生代大小也是 332MB
Q:怎么都是 332MB,动态调整新生代和老年代的机制没生效吧
Q:332 这个数字很有内涵,google young 332 根据下面两篇博文找到了缘由:
默认情况下和 cpu 核数有关,ScaleForWordSize 的值大约是 64M * 4 * 13 / 10 = 332.8M,再做下对齐就得到 332.75M 了;( 见参考资料:CMS GC 默认新生代是多大?)
Q:回到之前的问题,为什么 newRatio 参数默认为 2 没有生效
A:想起来 jdk 1.8 新生代默认的收集器并不是 CMS,是 ParallelGC。
于是继续 google 1.8 cms newRatio,找到了一篇 JVM bug 报告,在 1.8 中使用 CMS 收集器会导致默认的 newRatio 不生效,解决办法:在启动参数中显式配置一次,或者将新生代大小设置为固定值.https://bugs.openjdk.java.net/browse/JDK-8153578
Q:为什么动态调整没有生效
在 JDK1.7 中如果开启了 -XX:+UseAdaptiveSizePolicy 配置项,JVM 将会动态调整 Java 堆中各个区域的大小以及进入老年代的年龄,–XX:NewRatio 和 -XX:SurvivorRatio 将会失效. 而 JDK1.8 是默认开启 -XX:+UseAdaptiveSizePolicy 配置项的. 但使用 CMS 收集垃圾时会关闭 UseAdaptiveSizePolicy.
最后回顾一下整个问题:
1.jdk 1.8 中使用 CMS 收集器,UseAdaptiveSizePolicy 参数会被设置为 false,导致 young 区和 old 区大小不会动态调整
2.jdk 1.8 中使用 CMS 收集器,默认的 newRatio=2 不会生效,需要显示配置此参数或者配置 young 大小。否则按照 cpu 核心数量计算 young 大小:64M * cpu 核心数 * 13 / 10
3.批量任务每次任务量过大,短时间内创建大量对象且不释放,导致 jvm 疯狂的 young gc
4.频繁 young gc(100 次 / 秒)导致 CPU 使用率过高,系统吞吐量下降
三、解决方案
1.显式调整新生代大小
将 newRatio 调整为 3
2.离线任务错峰执行
批量任务调整为非业务高峰期执行
3.代码优化
- 减少定时任务每次执行的任务量
- 降低定时任务执行频率
- 大方法拆解:方法如果过长,在执行的过程中早期创建的对象没有释放,无法回收;抽象拆解成小方法,执行完便释放临时对象引用
在发布之后,CPU 使用率和 GC 次数回到合理的范围内
按小时统计在 24 分完成发布并开启定时任务:
随后系统的运行情况:
总结
- 在启动服务时即便配置了 JVM 参数,在启动后也要检查一下是否生效,因为 JVM 中有一些隐式规则
- 发现问题时使用工具快速定位问题,这次使用 arthas 查看资源占用的实际情况
- 避免在代码中创建大对象,或者批量创建大量对象
- 避免方法过长,导致临时对象无法及时回收
- 在业务高峰期关注服务监控指标
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/211989.html