前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一次频繁cms gc问题的排查

一次频繁cms gc问题的排查

作者头像
LNAmp
发布2018-09-05 15:21:19
1.4K0
发布2018-09-05 15:21:19
举报

前言

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

输出启动参数

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

代码语言:javascript
复制
 -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的日志

代码语言:javascript
复制
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区的大小变化,大大们的回答如下:

代码语言:javascript
复制
大大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观察下使用情况如下:

代码语言:javascript
复制
    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分析

代码语言:javascript
复制
检测到的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区的内存。

参考

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.04.03 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 输出启动参数
  • GC日志
    • A机器
      • B机器
        • 问题解决
        • 题外话
          • system gc
            • cms fail
              • concurrent mode failure
              • promotion failed
          • 参考
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档