本文首发于公众号【 程序员大帝 】,关注第一时间获取git
一块儿码动人生,成为Coding King!!!程序员
GitHub上已经开源 github.com/BeKingCodin… , 一线大厂面试核心知识点、个人联系方式和技术交流群,欢迎Star和完善github
一般来讲,一个系统在上线以前应该通过多轮的调试,在测试服务器上稳定的运行过一段时间。咱们知道 Full GC 会致使 Stop The World 状况的出现,严重影响性能,因此一个性能良好的 JVM,应该几天才会发生一次 Full GC,或者最多一天几回而已。面试
可是昨天晚上忽然收到短信通知,显示线上部署的四台机器所有卡死,服务所有不可用,赶忙查看问题!数据库
涉及到相似的错误,最开始三板斧确定是查看 JVM 的状况。不少中小型公司没有创建可视化的监控平台,好比Zabbix、Ganglia、Open-Falcon、Prometheus等等,没办法直接可视化看到JVM各个区域的内存变化,GC次数和GC耗时。服务器
不过不用怕,我们用 jstat 这种工具也能够。言归正传,排查了线上问题以后,发现居然是服务器上面,JVM 这段时间疯狂 Full GC,一天搞了几十次,直接把系统卡死了!数据结构
破案以前,咱们先来要保护下案发现场并进行排查。工具
机器配置 | 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。此时整个系统的内存模型图以下所示:
根据对案发现场的排查,咱们能够还原线上系统的 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,因此咱们还要有更多的证据去验证咱们的判断。
老年代里到底为何会有那么多的对象?
面对这个问题,说句实话,仅仅根据可视化监控和推论是绝对无法往下分析了,由于咱们并不知逋老年代里到底为何会有那么多的对象。这个时候就有必要让线上系统从新运行,借助 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 !
分析到这里,后面的过程就很简单了,咱们能够经过 jmap 工具,dump 出内存快照,而后再用 jhat 或者 Visual VM 之类的可视化工具来分析就能够了。
经过内存快照的分析,直接定位出来那个几百MB的大对象,发现居然是个Map之类的数据结构,这是什么鬼?
返回头去开始撸代码,发现是从数据库里查出来的数据存放在了这个Map里,没有好办法,再继续地毯式排查。
最后发现居然是有条坑爹的 SQL 语句没加 where条件!!不知道是手滑仍是忘了,测试的时候这个分支也没走到(画外音:这段代码的开发和测试都不是我)
没有 where 条件,每次查询确定会有超出预期的大量数据,致使了每隔一段时间就会搞出几个上百 MB 的大对象,这些对象所有直接进入老年代,而后触发 Full GC !
破案定位嫌疑人最困难,在知道凶手后,靠着满大街的摄像头,抓人就是分分钟的事情。因此咱们排查到这里,这个案例如何解决已经很是简单了。
(1)解决代码中的 bug,把错误的 SQL 修复,一劳永逸完全解决这几百 MB 大对象进入老年代的问题。
(2)以防万一,对堆内存空间进行扩容,而后再把-XX:CMSInitiatingOccupancyFraction 参数从 62 调高,使得老年代的使用率更高时才会触发 Full GC。
这两个步骤优化完毕以后,线上系统基本上表现就很是好了。
本文经过一个线上系统卡死的现象,详细地定位并剖析了产生问题的缘由。也证实了要成为一个优秀的程序员,不光对语言自己要有所了解,还要对 JVM 调优这样偏底层的知识有所涉猎,这对排查问题会有很是大的帮助。同时完善的监控很是重要,经过提早告警,能够将问题扼杀在摇篮里!
好了各位,以上就是这篇文章的所有内容了,能看到这里的人呀,都是人才。
我后面会持续更新《Offer收割机》系列和互联网经常使用技术栈相关的文章,很是感谢各位老板能看到这里,若是这个文章写得还不错,以为我有点东西的话,求点赞👍 求关注❤️ 求分享👥 对我来讲真的很是有用!!!
创做不易,各位的支持和承认,就是我创做的最大动力,咱们下篇文章见!
Craig无忌 | 文 【原创】【转载请联系本人】 若是本篇博客有任何错误,请批评指教,不胜感激 !
《Offer收割机》系列会持续更新,能够关注个人公众号「 程序员大帝 」第一时间阅读和催更(公众号比博客早一到两篇哟),本文GitHub上已经收录 github.com/BeKingCodin… ,会对一线大厂面试要求的核心知识点持续讲解、并有对标阿里P7级别的成长体系脑图,欢迎加入技术交流群,咱们一块儿有点东西。