YGC导致CPU负载过高的排查与解决

概述

在发现XXX系统的负载过高后确定解决方案,本文记录了整个过程。先说结论:

  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 导致 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

(0)
上一篇 2021年12月14日
下一篇 2021年12月14日

相关推荐

发表回复

登录后才能评论