一次频繁cms gc问题的排查

前言

前几天收到线上某机器cms-initial-remark次数超过13次报警,这台机器长时间没有过新功能上线,为啥会出现频繁cms gc告警呢,遂一起排查。

输出启动参数

登陆了问题发生的机器。考虑到是gc问题,首先想到的是输出虚拟机的参数,使用ps或者jps找到java进程,然后使用jinfo输出启动参数,启动参数如下:

 -Djava.util.logging.config.file=/home/admin/xxx/.default/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Dcatalina.vendor=alibaba -Djava.security.egd=file:/dev/./urandom -Dlog4j.defaultInitOverride=true -Dorg.apache.tomcat.util.http.ServerCookie.ALLOW_EQUALS_IN_VALUE=true -Dorg.apache.tomcat.util.http.ServerCookie.ALLOW_HTTP_SEPARATORS_IN_V0=true -Xms4g -Xmx4g -Xmn2g -XX:PermSize=96m -XX:MaxPermSize=256m -XX:SurvivorRatio=10 -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:+CMSClassUnloadingEnabled -XX:CMSMaxAbortablePrecleanTime=5000 -XX:CMSInitiatingOccupancyFraction=80 -XX:-HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/admin/logs -Xloggc:/home/admin/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Djava.awt.headless=true -Dsun.net.client.defaultConnectTimeout=10000 -Dsun.net.client.defaultReadTimeout=30000 -XX:MaxDirectMemorySize=1g -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExplicitGCInvokesConcurrent -Dsun.rmi.dgc.server.gcInterval=2592000000 -Dsun.rmi.dgc.client.gcInterval=2592000000 -XX:ParallelGCThreads=4 -DJM.LOG.PATH=/home/admin/logs -DJM.SNAPSHOT.PATH=/home/admin/snapshots -Dfile.encoding=GB18030 -Dhsf.publish.delayed=false -Dhsf.server.ip=11.183.92.169 -Dproject.name=xxx -Djava.endorsed.dirs=/opt/taobao/tomcat/endorsed -Dcatalina.logs=/home/admin/xxx/logs/catalina -Dcatalina.base=/home/admin/xxx/.default -Dcatalina.home=/opt/taobao/tomcat -Djava.io.tmpdir=/home/admin/xxx/.default/temp

同时确定了机器jdk的版本仍然是1.6

挑选出与gc有关的启动参数分析一下:

  • -Xms 4g -Xmx4g,最小最大堆内存为4g,锁定堆大小,不动态调整。
  • -Xmn2g,年轻代占2g。
  • -XX:SurvivorRatio=10,表示两个survivor : eden = 2 : 10,单个survivor占年轻代的1/12
  • -XX:PermSize=96m -XX:MaxPermSize=256m,永久代初始值为96m,最大值256m
  • -XX:+UseConcMarkSweepGC 使用cms gc收集算法
  • -XX:+UseCMSCompactAtFullCollection 在发生‘full gc’时对老年大内存空间进行整理压缩
  • -XX:+CMSClassUnloadingEnabled 在cms gc时对永久代进行回收(卸载类或者卸载类加载器)
  • -XX:CMSMaxAbortablePrecleanTime=5000 AbortablePreclean是cms过程中的一个阶段,用于平衡ygc 和remark阶段(避免连续两次stw),设定该阶段最长时间为5s
  • -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly 合起来用表示老年代used占capacity 80%的时候开启cms collection cycle.
  • -XX:MaxDirectMemorySize=1g 最大直接内存大小为1g
  • -XX:+ExplicitGCInvokesConcurrent,将显示调用System.gc()转化成一次cms gc(本应发生full gc)。
  • XX:ParallelGCThreads=4 并行收集的gc线程数为4,所以cms线程数ConcGCT
    hreads = (3 + ParallelGCThreads) / 1 = 1

GC日志

A机器

要定位gc问题,最核心的就是看gc日志定位问题。用工具dump了gc log分析,结果如下:

不看不知道,一看吓一跳。

  • cms gc的cycle数量达到了惊人的6543次
  • 存在4次Mix GC,并且由‘promotion failed’原因导致
  • 存在一次perm区溢出的cms gc

存在的问题已经比较清楚了

  • 频繁发生cms gc
  • 存在一定程度的promotion failed(这种gc的伤害比较大,应该尽量避免)
  • 存在意思perm溢出

那为啥会频繁的开始cms collection cycle呢,先挑选几条cms的日志

2017-10-12T22:35:56.602+0800: 777539.404: [GC [1 CMS-initial-mark: 950754K(2097152K)] 1939671K(4019584K), 0.7363400 secs] [Times: user=0.73 sys=0.01, real=0.74 secs]
2017-10-12T22:35:57.339+0800: 777540.141: [CMS-concurrent-mark-start]
2017-10-12T22:35:57.988+0800: 777540.791: [GC 777540.791: [ParNew: 1815272K->70343K(1922432K), 0.0484420 secs] 2766027K->1021205K(4019584K), 0.0486410 secs] [Times: user=0.15 sys=0.00, real=0.05 secs]
2017-10-12T22:35:59.137+0800: 777541.940: [CMS-concurrent-mark: 1.748/1.799 secs] [Times: user=4.08 sys=0.08, real=1.80 secs]
2017-10-12T22:35:59.137+0800: 777541.940: [CMS-concurrent-preclean-start]
2017-10-12T22:35:59.149+0800: 777541.952: [CMS-concurrent-preclean: 0.011/0.012 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2017-10-12T22:35:59.149+0800: 777541.952: [CMS-concurrent-abortable-preclean-start]

由于jvm并没有把发生cms gc的原因给输出,所以只能结合条件倒推下。我在jvm群里咨询了几位大大想问下cms gc前后old区的大小变化,大大们的回答如下:

大大1:
看gc log, initial mark 的记录是cms 触发时的used和capacity,是之前的
final remark的记录是之后的,我一般是这么理解的

大大2:
整个cms gc是从initial mark开始到remark结束,initial mark告诉你触发cms的old区大小,remark会告诉你cms后old区的使用情况

看了下日志:
**[GC [1 CMS-initial-mark: 950754K(2097152K)] ** 表示说开始gc之前old的大小为950754K,大约占整个old区的45%左右,但是结合 -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly 来看,触发这台cms gc的原因不是old区内存占用超过80%。

那么问题来了,触发这次cms gc的原因到底是什么呢?咨询了jvm群的大大,google了下外网和内网文章。1.6版本的cms gc主要原因如下:

  • 老年代空间使用率超过设定值
  • 永久代空间使用率超过设定值
  • System.gc()调用并且设置了-XX:+ExplicitGCInvokesConcurrent 将一次full gc转化为cms gc

目前已经排除第一条,那么第二条是否满足呢。需要注意的是perm区的阈值由-XX:CMSInitiatingPermOccupancyFraction指定,该值如果不设置的话是92,使用jstat -gcutil观察下使用情况如下:

    S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT
  0.00  61.24  14.84  45.36  92.67  17167  771.384 14136 7486.615 8257.999
  0.00  61.24  14.99  45.36  92.67  17167  771.384 14136 7486.615 8257.999
  0.00  61.24  15.09  45.36  92.67  17167  771.384 14136 7486.615 8257.999
  0.00  61.24  15.19  45.36  92.67  17167  771.384 14136 7486.615 8257.999
  0.00  61.24  15.27  45.36  92.67  17167  771.384 14137 7486.821 8258.205

可以看到Perm区的占比为92.67超过默认值92,所以cms gc会一直被触发。至于FGC次数为何那么大,可以参考R大的[文章](http://rednaxelafx.iteye.com/blog/1108768),cms 的initial-mark和remark都会计算到FGC次数里,所以jstat FGC次数可以理解为stw的次数吧。

从上述日志基本可以确定这台机器的频繁gc原因是由于perm区占用率大导致的。

B机器

在分析了这台机器是由于perm占用导致的cms gc后,我又查看了另外一台机器,发现了另外一个问题。
dump了另外一台机器的gc log分析

检测到的GC问题
筛选出了【9】条疑似问题的GC,跳过了【4307】条可能正常的GC
存在【0】次Full GC及【3】次Mix GC(Mix GC相当于Full GC,一次stop,多代进行回收)
【66.0%】【2/3】的Mix GC是因为晋升失败导致的
存在【2】次晋升失败的情况
过滤出来的GC中CMS GC比例占了【44%】【4/9】
调优建议
预测到您可能没有设置-XX:+UseCMSInitiatingOccupancyOnly,如果设置了请检查-XX:CMSInitiatingOccupancyFraction的值是否小于83,如果这个条件满足,说明老生代GC效果不给力,或者新生代GC过于频繁晋升对象多,可尝试调大CMSInitiatingOccupancyFraction或者调大老生代的大小

结合日志和之前的分析可疑看出来这台机器的cms gc主要是由于老年代占用过大以及比较碎片化导致的。

问题解决

原因清楚了之后调整就比较简单了,最终是将Xmx和Xms调整到了5g,将permsize和MaxPermSize调整到了384M。观察了几天没有再出现频繁cms gc的问题。

题外话

system gc

该机器的频繁cms gc原因找到了,再多分析一下cms gc的另外一个原因(System.gc)。

system gc主要有几个原因:

  • rmi定时调用
  • 代码调用 可以通过btrace或者arthas定位调用代码
  • 堆外内存清理
  • Dsun.rmi.dgc.server.gcInterval=2592000000 -Dsun.rmi.dgc.client.gcInterval=2592000000

cms fail

concurrent mode failure

gc log会出现一些问题日志,表明cms遭遇了问题,首先就是‘concurrent mode failure’.

日志如下:

267.006: [GC 267.006: [ParNew: 629120K->629120K(629120K), 0.0000200 secs]
267.006: [CMS267.350: [CMS-concurrent-mark: 2.683/2.804 secs]
[Times: user=4.81 sys=0.02, real=2.80 secs]
(concurrent mode failure):
1378132K->1366755K(1398144K), 5.6213320 secs]
2007252K->1366755K(2027264K),
[CMS Perm : 57231K->57222K(95548K)], 5.6215150 secs]
[Times: user=5.63 sys=0.00, real=5.62 secs]
当minor gc发生了,但是old区中没有足够的空间容纳需要晋升的对象时,cms会触发一次full gc。此时STW,所有应用线程都会停止,old区中dead对象会被清理,上述日志中表明old区清理到了136MB,但是full gc STW了5.62s。这个操作是单线程进行的,所以花费了很长时间。

promotion failed

第二个问题就是当old区有足够的空间给待晋升的对象时,但是由于空闲内存过于碎片化导致的。

6043.903: [GC 6043.903:
[ParNew (promotion failed): 614254K->629120K(629120K), 0.1619839 secs]
6044.217: [CMS: 1342523K->1336533K(2027264K), 30.7884210 secs]
2004251K->1336533K(1398144K),
[CMS Perm : 57231K->57231K(95548K)], 28.1361340 secs]
[Times: user=28.13 sys=0.38, real=28.13 secs]
日志表明,cms开始了一次ygc,而且认为old区中有足够的空间来容纳待晋升的对象(否则,将会导致concurrent mode failure问题),然后由于old区内存过于碎片化,导致cms无法晋升这些对象。

所以,在minor gc的过程中,cms尝试去清理和压缩整理old区。好消息是heap最终会被重新压缩整理,碎片化问题得以解决。但是会带来一个28秒的停顿时间。这个时间比发生‘concurrent mode failure’时消耗的时间要长,因为此时整个heap进行了压缩整理,而concurrent mode failure只free掉old区的内存。

参考

  • 一次CMS GC问题排查过程(理解原理+读懂GC日志)
  • jstat显示的full GC次数与CMS周期的关系
  • 《Java Performance The Definitive Guide》GC部分读书笔记

你可能感兴趣的:(一次频繁cms gc问题的排查)