线上服务的GC问题,是Java程序很是典型的一类问题,很是考验工程师排查问题的能力。同时,几乎是面试必考题,可是能真正答好此题的人并很少,要么原理没吃透,要么缺少实战经验。程序员
过去半年时间里,咱们的广告系统出现了屡次和GC相关的线上问题,有Full GC过于频繁的,有Young GC耗时过长的,这些问题带来的影响是:GC过程当中的程序卡顿,进一步致使服务超时从而影响到广告收入。面试
这篇文章,我将以一个FGC频繁的线上案例做为引子,详细介绍下GC的排查过程,另外会结合GC的运行原理给出一份实践指南,但愿对你有所帮助。内容分红如下3个部分:算法
一、从一次FGC频繁的线上案例提及数组
二、GC的运行原理介绍安全
三、排查FGC问题的实践指南微信
去年10月份,咱们的广告召回系统在程序上线后收到了FGC频繁的系统告警,经过下面的监控图能够看到:平均每35分钟就进行了一次FGC。而程序上线前,咱们的FGC频次大概是2天一次。下面,详细介绍下该问题的排查过程。架构
1. 检查JVM配置并发
经过如下命令查看JVM的启动参数:
ps aux | grep "applicationName=adsearch"app
-Xms4g -Xmx4g -Xmn2g -Xss1024K
-XX:ParallelGCThreads=5
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+UseCMSCompactAtFullCollection
-XX:CMSInitiatingOccupancyFraction=80框架
能够看到堆内存为4G,新生代为2G,老年代也为2G,新生代采用ParNew收集器,老年代采用并发标记清除的CMS收集器,当老年代的内存占用率达到80%时会进行FGC。
进一步经过 jmap -heap 7276 | head -n20 能够得知新生代的Eden区为1.6G,S0和S1区均为0.2G。
2. 观察老年代的内存变化
经过观察老年代的使用状况,能够看到:每次FGC后,内存都能回到500M左右,所以咱们排除了内存泄漏的状况。
3. 经过jmap命令查看堆内存中的对象
经过命令 jmap -histo 7276 | head -n20
上图中,按照对象所占内存大小排序,显示了存活对象的实例数、所占内存、类名。能够看到排名第一的是:int[],并且所占内存大小远远超过其余存活对象。至此,咱们将怀疑目标锁定在了 int[] .
4. 进一步dump堆内存文件进行分析
锁定 int[] 后,咱们打算dump堆内存文件,经过可视化工具进一步跟踪对象的来源。考虑堆转储过程当中会暂停程序,所以咱们先从服务管理平台摘掉了此节点,而后经过如下命令dump堆内存:
jmap -dump:format=b,file=heap 7276
经过JVisualVM工具导入dump出来的堆内存文件,一样能够看到各个对象所占空间,其中int[]占到了50%以上的内存,进一步往下即可以找到 int[] 所属的业务对象,发现它来自于架构团队提供的codis基础组件。
5. 经过代码分析可疑对象
经过代码分析,codis基础组件每分钟会生成约40M大小的int数组,用于统计TP99 和 TP90,数组的生命周期是一分钟。而根据第2步观察老年代的内存变化时,发现老年代的内存基本上也是每分钟增长40多M,所以推断:这40M的int数组应该是重新生代晋升到老年代。
咱们进一步查看了YGC的频次监控,经过下图能够看到大概1分钟有8次左右的YGC,这样基本验证了咱们的推断:由于CMS收集器默认的分代年龄是6次,即YGC 6次后还存活的对象就会晋升到老年代,而codis组件中的大数组生命周期是1分钟,恰好知足这个要求。
至此,整个排查过程基本结束了,那为何程序上线前没出现此问题呢?经过上图能够看到:程序上线前YGC的频次在5次左右,这次上线后YGC频次变成了8次左右,从而引起了此问题。
6. 解决方案
为了快速解决问题,咱们将CMS收集器的分代年龄改为了15次,改完后FGC频次恢复到了2天一次,后续若是YGC的频次超过每分钟15次还会再次触发此问题。固然,咱们最根本的解决方案是:优化程序以下降YGC的频率,同时缩短codis组件中int数组的生命周期,这里就不作展开了。
上面整个案例的分析过程当中,其实涉及到不少GC的原理知识,若是不懂得这些原理就着手处理,其实整个排查过程是很抓瞎的。
这里,我选择几个最核心的知识点,展开介绍下GC的运行原理,最后再给出一份实践指南。
1. 堆内存结构
你们都知道: GC分为YGC和FGC,它们均发生在JVM的堆内存上。先来看下JDK8的堆内存结构:
能够看到,堆内存采用了分代结构,包括新生代和老年代。新生代又分为:Eden区,From Survivor区(简称S0),To Survivor区(简称S1区),三者的默认比例为8:1:1。另外,新生代和老年代的默认比例为1:2。
堆内存之因此采用分代结构,是考虑到绝大部分对象都是短生命周期的,这样不一样生命周期的对象可放在不一样的区域中,而后针对新生代和老年代采用不一样的垃圾回收算法,从而使得GC效率最高。
2. YGC是何时触发的?
大多数状况下,对象直接在年轻代中的Eden区进行分配,若是Eden区域没有足够的空间,那么就会触发YGC(Minor GC),YGC处理的区域只有新生代。由于大部分对象在短期内都是可收回掉的,所以YGC后只有极少数的对象能存活下来,而被移动到S0区(采用的是复制算法)。
当触发下一次YGC时,会将Eden区和S0区的存活对象移动到S1区,同时清空Eden区和S0区。当再次触发YGC时,这时候处理的区域就变成了Eden区和S1区(即S0和S1进行角色交换)。每通过一次YGC,存活对象的年龄就会加1。
3. FGC又是何时触发的?
下面4种状况,对象会进入到老年代中:
一、YGC时,To Survivor区不足以存放存活的对象,对象会直接进入到老年代。
二、通过屡次YGC后,若是存活对象的年龄达到了设定阈值,则会晋升到老年代中。
三、动态年龄断定规则,To Survivor区中相同年龄的对象,若是其大小之和占到了 To Survivor区一半以上的空间,那么大于此年龄的对象会直接进入老年代,而不须要达到默认的分代年龄。
四、大对象:由-XX:PretenureSizeThreshold启动参数控制,若对象大小大于此值,就会绕过新生代, 直接在老年代中分配。
当晋升到老年代的对象大于了老年代的剩余空间时,就会触发FGC(Major GC),FGC处理的区域同时包括新生代和老年代。除此以外,还有如下4种状况也会触发FGC:
一、老年代的内存使用率达到了必定阈值(可经过参数调整),直接触发FGC。
二、空间分配担保:在YGC以前,会先检查老年代最大可用的连续空间是否大于新生代全部对象的总空间。若是小于,说明YGC是不安全的,则会查看参数 HandlePromotionFailure 是否被设置成了容许担保失败,若是不容许则直接触发Full GC;若是容许,那么会进一步检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若是小于也会触发 Full GC。
三、Metaspace(元空间)在空间不足时会进行扩容,当扩容到了-XX:MetaspaceSize 参数的指定值时,也会触发FGC。
四、System.gc() 或者Runtime.gc() 被显式调用时,触发FGC。
4. 在什么状况下,GC会对程序产生影响?
无论YGC仍是FGC,都会形成必定程度的程序卡顿(即Stop The World问题:GC线程开始工做,其余工做线程被挂起),即便采用ParNew、CMS或者G1这些更先进的垃圾回收算法,也只是在减小卡顿时间,而并不能彻底消除卡顿。
那到底什么状况下,GC会对程序产生影响呢?根据严重程度从高到底,我认为包括如下4种状况:
一、FGC过于频繁:FGC一般是比较慢的,少则几百毫秒,多则几秒,正常状况FGC每隔几个小时甚至几天才执行一次,对系统的影响还能接受。可是,一旦出现FGC频繁(好比几十分钟就会执行一次),这种确定是存在问题的,它会致使工做线程频繁被中止,让系统看起来一直有卡顿现象,也会使得程序的总体性能变差。
二、YGC耗时过长:通常来讲,YGC的总耗时在几十或者上百毫秒是比较正常的,虽然会引发系统卡顿几毫秒或者几十毫秒,这种状况几乎对用户无感知,对程序的影响能够忽略不计。可是若是YGC耗时达到了1秒甚至几秒(都快遇上FGC的耗时了),那卡顿时间就会增大,加上YGC自己比较频繁,就会致使比较多的服务超时问题。
三、FGC耗时过长:FGC耗时增长,卡顿时间也会随之增长,尤为对于高并发服务,可能致使FGC期间比较多的超时问题,可用性下降,这种也须要关注。
四、YGC过于频繁:即便YGC不会引发服务超时,可是YGC过于频繁也会下降服务的总体性能,对于高并发服务也是须要关注的。
其中,「FGC过于频繁」和「YGC耗时过长」,这两种状况属于比较典型的GC问题,大几率会对程序的服务质量产生影响。剩余两种状况的严重程度低一些,可是对于高并发或者高可用的程序也须要关注。
经过上面的案例分析以及理论介绍,再总结下FGC问题的排查思路,做为一份实践指南供你们参考。
1. 清楚从程序角度,有哪些缘由致使FGC?
一、大对象:系统一次性加载了过多数据到内存中(好比SQL查询未作分页),致使大对象进入了老年代。
二、内存泄漏:频繁建立了大量对象,可是没法被回收(好比IO对象使用完后未调用close方法释放资源),先引起FGC,最后致使OOM.
三、程序频繁生成一些长生命周期的对象,当这些对象的存活年龄超过度代年龄时便会进入老年代,最后引起FGC. (即本文中的案例)
四、程序BUG致使动态生成了不少新类,使得 Metaspace 不断被占用,先引起FGC,最后致使OOM.
五、代码中显式调用了gc方法,包括本身的代码甚至框架中的代码。
六、JVM参数设置问题:包括总内存大小、新生代和老年代的大小、Eden区和S区的大小、元空间大小、垃圾回收算法等等。
2. 清楚排查问题时能使用哪些工具
一、公司的监控系统:大部分公司都会有,可全方位监控JVM的各项指标。
二、JDK的自带工具,包括jmap、jstat等经常使用命令:
查看堆内存各区域的使用率以及GC状况
jstat -gcutil -h20 pid 1000查看堆内存中的存活对象,并按空间排序
jmap -histo pid | head -n20dump堆内存文件
jmap -dump:format=b,file=heap pid三、可视化的堆内存分析工具:JVisualVM、MAT等
3. 排查指南
一、查看监控,以了解出现问题的时间点以及当前FGC的频率(可对比正常状况看频率是否正常)
二、了解该时间点以前有没有程序上线、基础组件升级等状况。
三、了解JVM的参数设置,包括:堆空间各个区域的大小设置,新生代和老年代分别采用了哪些垃圾收集器,而后分析JVM参数设置是否合理。
四、再对步骤1中列出的可能缘由作排除法,其中元空间被打满、内存泄漏、代码显式调用gc方法比较容易排查。
五、针对大对象或者长生命周期对象致使的FGC,可经过 jmap -histo 命令并结合dump堆内存文件做进一步分析,须要先定位到可疑对象。
六、经过可疑对象定位到具体代码再次分析,这时候要结合GC原理和JVM参数设置,弄清楚可疑对象是否知足了进入到老年代的条件才能下结论。
这篇文章经过线上案例并结合GC原理详细介绍了FGC的排查过程,同时给出了一份实践指南。
后续会以相似的方式,再分享一个YGC耗时过长的案例,但愿能帮助你们吃透GC问题排查,若是以为本文对你有帮助,请你们关注个人我的公众号!
做者简介:程序员,985硕士,前亚马逊Java工程师,现58转转技术总监。持续分享技术和管理方向的文章。若是感兴趣,可微信扫描下面的二维码关注个人公众号:『IT人的职场进阶』