重大事故!线上系统频繁卡死,凶手居然是 Full GC ?

本文首发于公众号【 程序员大帝 】,关注第一时间获取git

一块儿码动人生,成为Coding King!!!程序员

GitHub上已经开源 github.com/BeKingCodin… , 一线大厂面试核心知识点、个人联系方式和技术交流群,欢迎Star和完善github

在这里插入图片描述

01 案发现场

一般来讲,一个系统在上线以前应该通过多轮的调试,在测试服务器上稳定的运行过一段时间。咱们知道 Full GC 会致使 Stop The World 状况的出现,严重影响性能,因此一个性能良好的 JVM,应该几天才会发生一次 Full GC,或者最多一天几回而已。面试

可是昨天晚上忽然收到短信通知,显示线上部署的四台机器所有卡死,服务所有不可用,赶忙查看问题!数据库

涉及到相似的错误,最开始三板斧确定是查看 JVM 的状况。不少中小型公司没有创建可视化的监控平台,好比Zabbix、Ganglia、Open-Falcon、Prometheus等等,没办法直接可视化看到JVM各个区域的内存变化,GC次数和GC耗时。服务器

不过不用怕,我们用 jstat 这种工具也能够。言归正传,排查了线上问题以后,发现居然是服务器上面,JVM 这段时间疯狂 Full GC,一天搞了几十次,直接把系统卡死了!数据结构

02 排查现场

破案以前,咱们先来要保护下案发现场并进行排查。工具

在这里插入图片描述

机器配置 2核 4G
JVM堆内存大小 2G
系统运行时间 1天
Full GC出现次数和单次耗时 48次,300ms左右
Young GC出现次数和单次耗时 4000屡次,50ms

这样看起来,系统的性能是至关差了,一分钟三次 Young GC,每半小时就一次 Full GC,接下来咱们再看看 JVM 的参数。可能有的同窗每次见到这么多参数都会头大,但其实每个参数的背后都会透漏着蛛丝马迹。我这里摘取关键参数以下:性能

CMSInitiatingOccupancyFraction 62
SurvivorRatio 5
Xmx 1536M
Xmx 1536M

简单解读一下,根据参数能够看出来,这台 4G 的机器上为 JVM 的堆内存是设置了 1.5G 左右的大小。新生代和老年代默认会按照 1:2 的比例进行划分,分别对应 512M 和 1G。测试

一个重要的参数 XXiSurvivorRatio 设置为5,它所表明的是新生代中Eden:Survivor1 :Survivor2的比例是 5:1:1。因此此时Eden区域大体为365M,每一个Survivor区域大体为70MB。

还有一个很是关键的参数,那就是 CMSInitiatingOccupancyFraction 设置为了62。它意味着一旦老年代内存占用达到 62%,也就是存在 620MB 左右对象时,就会触发一次 Full GC。此时整个系统的内存模型图以下所示:

在这里插入图片描述

03 还原现场

根据对案发现场的排查,咱们能够还原线上系统的 GC 运行状况了,分析一下线上的 JVM 到底出现了什么情况。

首先咱们知道每分钟会发生 3 次 Young GC,说明系统运行 20 秒后就会把 Eden 区塞满,Eden 区一共有 356MB 的空间,所以平均下来系统每秒钟会产生 20MB 左右大小的对象。

接着咱们根据每半小时触发一次 Full GC 的推断,以及 “-XX:CMSmitiatingOccupancyFraction=62” 参数的设置,老年代有 1G 的空间,因此应该是在老年代有 600多MB 左右的对象时就会触发一次 Full GC。

看到这里,有的同窗可能马上下结论,以为确定是由于 Survivor 区域过小了,致使 Young GC 后的存活对象太多放不下,就一直有对象流入老年代,进而致使后来触发的 Full GC ?

实际上分析到这里,绝对不能草率下这个判断。

由于老年代里为何有那么多的对象?确实有多是由于每次 Young GC后的存活对象较多,Survivor区域过小,放不下了。

但也有多是长时间存活的对象太多了,都积累在老年代里,始终回收不掉,进而致使老年代很容易就达到 62% 的占比触发 Full GC,因此咱们还要有更多的证据去验证咱们的判断。

04 破案开始

老年代里到底为何会有那么多的对象?

面对这个问题,说句实话,仅仅根据可视化监控和推论是绝对无法往下分析了,由于咱们并不知逋老年代里到底为何会有那么多的对象。这个时候就有必要让线上系统从新运行,借助 jstat 工具实时去观察 JVM 实际的运行状况。这个过程很是相似警察叔叔在破案时,会假设本身是凶手,尝试再现当时的场景。

这里省略具体的 jstat 工具操做过程,若是你们没有接触过百度下便可,很是简单。经过 jstat 的观察,咱们当时能够看到,每次 Young GC 事后升入老年代里的对象其实不多。

看到这个现象,我起初也很奇怪。由于经过 jstat 的追踪观察,并非每次Young GC 后都有 几十MB 对象进入老年代的,而是偶尔一次 Young GC 才会有 几十MB 对象进入老年代!

因此正常来讲,既然没有对象重新生代过渡到老年代,那么老年代就不至于快速的到达 62% 的占有率,从而致使 Full GC。那么老年代触发 Full GC 时候的几百 MB 对象到底从哪里来的?

在这里插入图片描述

仔细一想,其实答案已经呼之欲出了,那就是大对象!

必定是系统运行的时候,每隔一段时间就会忽然产生几百 MB 的大对象,因为新生代放不下,因此会直接进入老年代,而不会走 Eden 区域。而后再配合上年轻代还偶尔会有 Young GC 后几十 MB 对象进入老年代,因此不停地触发Full GC !

05 抓捕真凶

分析到这里,后面的过程就很简单了,咱们能够经过 jmap 工具,dump 出内存快照,而后再用 jhat 或者 Visual VM 之类的可视化工具来分析就能够了。

经过内存快照的分析,直接定位出来那个几百MB的大对象,发现居然是个Map之类的数据结构,这是什么鬼?

返回头去开始撸代码,发现是从数据库里查出来的数据存放在了这个Map里,没有好办法,再继续地毯式排查。

最后发现居然是有条坑爹的 SQL 语句没加 where条件!!不知道是手滑仍是忘了,测试的时候这个分支也没走到(画外音:这段代码的开发和测试都不是我)

没有 where 条件,每次查询确定会有超出预期的大量数据,致使了每隔一段时间就会搞出几个上百 MB 的大对象,这些对象所有直接进入老年代,而后触发 Full GC !

06 善后处理

破案定位嫌疑人最困难,在知道凶手后,靠着满大街的摄像头,抓人就是分分钟的事情。因此咱们排查到这里,这个案例如何解决已经很是简单了。

(1)解决代码中的 bug,把错误的 SQL 修复,一劳永逸完全解决这几百 MB 大对象进入老年代的问题。

(2)以防万一,对堆内存空间进行扩容,而后再把-XX:CMSInitiatingOccupancyFraction 参数从 62 调高,使得老年代的使用率更高时才会触发 Full GC。

这两个步骤优化完毕以后,线上系统基本上表现就很是好了。

07 总结

本文经过一个线上系统卡死的现象,详细地定位并剖析了产生问题的缘由。也证实了要成为一个优秀的程序员,不光对语言自己要有所了解,还要对 JVM 调优这样偏底层的知识有所涉猎,这对排查问题会有很是大的帮助。同时完善的监控很是重要,经过提早告警,能够将问题扼杀在摇篮里!

平常求赞

好了各位,以上就是这篇文章的所有内容了,能看到这里的人呀,都是人才。

我后面会持续更新《Offer收割机》系列和互联网经常使用技术栈相关的文章,很是感谢各位老板能看到这里,若是这个文章写得还不错,以为我有点东西的话,求点赞👍 求关注❤️ 求分享👥 对我来讲真的很是有用!!!

创做不易,各位的支持和承认,就是我创做的最大动力,咱们下篇文章见!

Craig无忌 | 文 【原创】【转载请联系本人】 若是本篇博客有任何错误,请批评指教,不胜感激 !


《Offer收割机》系列会持续更新,能够关注个人公众号「 程序员大帝 」第一时间阅读和催更(公众号比博客早一到两篇哟),本文GitHub上已经收录 github.com/BeKingCodin… ,会对一线大厂面试要求的核心知识点持续讲解、并有对标阿里P7级别的成长体系脑图,欢迎加入技术交流群,咱们一块儿有点东西。

在这里插入图片描述
相关文章
相关标签/搜索