线上 GC 故障:CMSGC 太频繁,你知道这是什么鬼?
背景
今天跟大家分享一个前几天在线上碰到的一个 GC 故障— "CMSGC 太频繁"。
不知道大家看到这条告警内容后,是什么感触?我当时是一脸懵逼的,一万个为什么萦绕心头。
什么是 CmsGc?CmsGc 太频繁又是什么意思?什么情况下会触发 CMSGC 太频繁这种告警?要怎么样去找到那个被频繁创建的对象?最后又需要怎么规避?
接下来这篇文章我会来回答一下:什么是 CMSGC 太频繁;整个排查过程与你分享;最后我们一起探讨一下一些规避手段。
什么是 CMSGC 太频繁
首先我觉得还是有必要解释清楚什么是 CMSGC 太频繁这个术语,相信不少小伙伴也是比较关心的。
如果你听过垃圾搜集器中有一款名为 CMS 垃圾搜集器,那就好理解了,所谓的 CMSGC 太频繁意思是说 CMS 垃圾搜集器在当下时间窗口垃圾收集的动作频次太快(平时老半天才回收一次或几次垃圾对象,现在可能一分钟就需要回收多次),大致就是这个意思。
关于 CMS 垃圾收集器的说明:
上述这张图中共有 7 种不同的垃圾搜集器,用连线表示它们彼此之间的搭配使用。
分割线上面部分是年轻代区域,像 Serial、ParNew、Parallel Scavenge 这三款垃圾收集器是用来搜集年轻代内存区域的垃圾收集器。
分割线下面部分是老年代区域,像 CMS、Serial Old、Parallel Old 这三款垃圾收集器用来收集老年代区域的垃圾收集器。
在实际线上配置场景中,我们一般通过 CMS+ParNew,采用分代收集(parNew 垃圾收集器用来收集年轻代区域,Cms 垃圾收集器用来收集老年代区域)来进行配置。
所以说 CMS 垃圾收集器是一款作用于老年代区域的垃圾收集器。
关于 CMS+ParNew 垃圾搜集器的配置说明:大家如果在 VM 启动配置参数中做如下配置:-XX:+UseConcMarkSweepGC.该配置项首先是激活 CMS 收集器(作用于老年代)。之后-XX:UseParNewGC 会自动开启,意味着年轻代将使用多线程并行垃圾收集器 parNew 进行回收。
原因分析
上文中,我给大家解释了 CMSGC 太频繁的意思。其实就是 CMS 垃圾搜集器对作用于老年代的垃圾对象进行回收,但频次太高,所以才触发了告警。
接下来给大家介绍一下引起对象进入老年代的几种场景,然后再给大家介绍一下几种触发 CMSGC 的情况。大家需要先搞明白有哪些情况对象会进入老年代,又达到什么标准作用于老年代的垃圾收集线程开始会对垃圾对象进行回收。
▲对象进入老年代的几种情况
新生代因为垃圾回收之后,因为存活对象太多,导致 Survivor 空间放不下,部分对象会进入老年代
大对象直接进入老年代
这里的大对象是指那些需要大量连续空间的 JAVA 对象,比如那种很长的字符串或数组对象。
长期存活的对象将进入老年代
对象在 Eden 出生,并经过第一次 YGC 后任然存活,并且能被 Survivor 空间容纳,将被移动到 Survivor 空间中,并且对象年龄设为 1。对象在 Survivor 空间每熬过一次 YGC,年龄就增加一岁,如果达到 15(默认)岁,对象就会进入老年代。
动态对象年龄判断
这点是对长期存活的对象进入老年代的补充。
其实不一定要必须满足所谓的存活对象年龄达到 15 岁才能进入老年代。如果一次 YGC 后,尽管 Survivor 区域有空间能容纳存活对象,但这批存活对象恰好存活的年龄相同,且加起来的大小总和大于 Survivor 空间的一半,这些对象照样会进入老年代。
▲触发 CMS 垃圾收集动作的几个时机
CMS 垃圾收集动作不可能实时发生,只有满足了相应条件,才会被触发。以下几点供你参考:
老年代可用的连续空间小于年轻代历次 YGC 后升入老年代的对象总和的平均大小,说明 YGC 后升入老年代的对象大小很可能超过了老年底当期可用的内存空间;触发 cmsgc 后再进行 ygc
ygc 之后有一批对象需要放入老年代,但老年代没有足够的空间存放了,需要触发一次 cmsgc
老年代的内存使用率超过 92%,也要触发 OLD 过程(通过参数控制-xx:+CMSInitiatingOccupancyFraction)
排查过程
这个章节详细也是不少小伙伴关心的内容,一旦发生了这种告警,那你肯定第一时间比较关心的内容是:到底老年代内存区域里面,什么对象会占据那么大的空间,找到它才是当下之急。
大家其实只要记住两个步骤,就能轻松找出问题对象。
▲步骤一:获取堆文件
获取堆文件,我总结了如下三个方式,供大家参考
配置 VM 参数
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOGDIR}/ 虚拟机在 OOM 异常之后会自动生成一份 dump 文件在本地 。
执行 jmap(Java 内存映像工具)命令
jdk 提供的命令行工具 jmap 能生成堆存储快照,jmap -dump:format=b,file=heapdump.hprof {进程 ID}
阿里开源性能诊断工具:Arthas
阿里开源的性能诊断工具 Arthas 通过命令 heapdump[类似 jmap 命令的 heap dump 功能]能生成堆快照文件。
详情大家可以参考官方说明文档:https://arthas.gitee.io/doc/heapdump.html
▲步骤二:分析堆文件
分析步骤一生成的堆文件,一般需要借助一些工具常见的有 MAT、Jvisualvm 等。
接下来作者用本次告警 dump 下来的堆文件,用 MAT 工具给大家演示一下具体查找问题对象的全过程。
MAT 是 Memory Analyzer tool 的缩写,是一种快速,功能丰富的 Java 堆分析工具,能帮助你查找内存泄漏和减少内存消耗。
很多情况下,我们需要处理测试提供的 hprof 文件,分析内存相关问题,那么 MAT 也绝对是不二之选。Eclipse 可以下载插件结合使用,也可以作为一个独立分析工具使用。
下载地址:eclipse.org/mat/downloa。如果安装过程中可能会碰到版本过低的问题,需要安装一下高版本 JDK 比如 11,最后设置一下安装路径即可。
相关视频解析:JVM实战调优教程
打开堆文件
如果你已经成功安装完 MAT。进入首页后就可以打开本地 hprof 文件了。
打开文件后,进入分析页
底部有三个功能块:Action、Reports、Step By Step。简单给大家介绍一下相应内容:
Actions:
Histogram 列出每个类所对应的对象个数,以及所占用的内存大小;Dominator Tree 以占用总内存的百分比的方式来列举出所有的实例对象,注意这个地方是直接列举出的对应的对象而不是类,这个视图是用来发现大内存对象的 Top Consumers:按照类和包分组的方式展示出占用内存最大的一个对象 Duplicate Classes:检测由多个类加载器所加载的类信息(用来查找重复的类)
Reports:
Leak Suspects:通过 MAT 自动分析当前内存泄露的主要原因
Top Components:Top 组件,列出大于总堆 1%的组件的报告
Step By Step:
Component Report:组件报告,分析属于公共根包或类加载器的对象
Histogram 选项
这里大家重点关注 Histogram 选项(列出每个类所对应的对象个数,以及所占用的内存大小)
DomainTree 选项(以占用总内存的百分比的方式来列举出所有的实例对象)
关注上述两个选项基本就能找到问题对象了。
解决方案
要避免发生 CMSGC 太频繁这种情况,我总结了以下 2 种方案:
如果你的程序代码书写正常,纯粹是真的应用流量太大,你部署的机器没办法抗住这波流量,这种情况发生 CMSGC 太频繁概率就很大了,甚至最终会导致 OOM 异常。对这种情况也只能横向扩充机器了,以均衡流量。
如果你的机器足够,线上流量也正常,但也发生了 cmsgc 太频繁,甚至 OOM 异常。那大概率是你的程序代码有问题,导致老年代区域聚集了大量垃圾对象,垃圾回收线程频繁回收那些无用的垃圾对象,最终可能还达不到回收的理想效果,那么这个时候你不得不分析堆里面被大量占据的对象,看看是不是程序代码问题导致老年代被堆满。
像作者文章开始出的这个案例,作者经过上述步骤分析后,发现是程序代码问题导致有大量对象进入老年代。(作者在应用中引入了一个 java8 的 Nashorn 组件,该组件的构建过程极其复杂,内部会创建很多个对象实例,因为作者的业务流量还是比较大的,每秒 2000+QPS),机器也是够的大概 10 台(每台 4C8G),分析发现内存中大量充斥着 Nashorn 相关代码,经过深入分析,其实这个 Nashorn 实例全局单例就可以了,不需要每次方法执行都构建一个实例,因为构建过程复杂且多对象,流量一高势必最终导致应用发生内存溢出等异常。
总结
OK,文章即将进入尾声,让我们一起来做个总结:
首先作者以一个自己亲身经历的 GC 故障为背景,跟大家介绍了一下什么是 CMSGC 太频繁这个术语,相信小伙伴如果下次自己碰到类似这种告警,能明白其含义。
其次作者也介绍了 CMSGC 太频繁一般作用的区域是老年代内存区域,有几种情况对象会从年轻代或直接进入老年代,以及老年代什么情况下会触发其垃圾回收动作。
然后作者也给大家介绍了该如何一步一步通过工具 MAT 去排查在堆文件里的问题对象。
最后我也总结了应该如何避免发生 GC 太频繁甚至 OOM 这类异常。如果程序代码一切正常,纯粹是瞬时流量太高才导致的 GC 动作加快,可以考虑临时增加服务器实例,分摊流量。不过很多问题可能都是程序员代码书写不正确才导致的,这个时候你应该首先找出问题对象,然后找出频繁创建对象的代码块。
原文:https://mp.weixin.qq.com/s/hrQv6esQ7QKWgwv1gLGIow
如果感觉本文对你有帮助,点赞关注支持一下,想要了解更多 Java 后端,大数据,算法领域最新资讯可以关注我公众号【架构师老毕】私信 666 还可获取更多 Java 后端,大数据,算法 PDF+大厂最新面试题整理+视频精讲
评论