记一次性困难的GC疑问排查!
背景
gc疑问不时是一个很难排查的疑问,但是他又是一个经常在咱们开发业务中出现的。这不,最近在我的名目中就出现了一个比拟奇葩的gc疑问,排查环节比拟繁琐,所以在这里分享一下这个整个排查环节,宿愿对大家有必定的协助
排查环节
确定GC出疑问
在某一天的上午突然出现了报警,发现是ZK断开了链接,
从图上看咱们这个失误是连续性的出现,最开局认为是zk出现了疑问,起初经过排查其余服务的zk并没有出现任何疑问。所以就疑心是外部的代码出现疑问造成的,钻研之后发现是zk出现了心跳超时状况才造成的断开链接,所以就疑心了两种状况:
假设网络有颤抖的话确实是会出现偶发性超时,但是很显著,其余一切的服务都没疑问,应该不是颤抖造成。所以机器应该是间歇性的一个卡死,普通出现这个状况首当其冲的就是咱们CPU被打满了,造成机器卡死,发现CPU并无疑问,而后就是咱们的gc带来的STW,会造成咱们的jvm进程卡顿。
观察之后确实是young gc很慢,造成咱们的JVM出现了GC卡顿,所以出现了这个现象。
排查要素
GC出现疑问普通来说两大法宝可以处置大局部疑问:
出现疑问之后我立马关上了GC日志,截图如下:
可以发现咱们的young gc曾经到达2.7s了,大家知道咱们的younggc是全程STW的,那就象征着每次gc就会卡顿2.7s,那么zk超时断开链接也就合乎反常了。再看了下这个gc搜集状况,每次也能齐全搜集。在日志中很显著在rootscanning的时期比拟长,过后对这个阶段不太相熟(前面会继续讲),所以不时也不明确为什么这样,在网上各种搜查,也没有论断。
这个时刻我在why哥群众号读到了一篇文章:倡导大家可以浏览一下这篇文章,这个文章中关键谈到了咱们jvm的一个提升,大家都知道咱们进入STW的时刻是要求一个安保点才可以的,而征询能否进入到安保点是要求消耗资源的,所以jvm在做jit提升的时刻会讲countedloop也就是计数循环提升成整个循环完结之后再进入安保点,在小米的技术文章中也提到了关系的疑问:《HBase实战:记一次性Safepoint造生长时期STW的踩坑之旅》。
看完这两个文章之后,我突然想到了咱们的代码也是counted loop的方式,所以就疑心有或许也是这个疑问造成的,马上启动代码提升,将for(int i= 0; i< n; i++) 中的int换成了long,就可以防止这种jit的提升,马上灰溜溜的将其上线,结果过了一天之后依然存在这个疑问,此时人都快解体,搞了半天原来不是这个疑问造成的。
定位疑问
关于G1之前只是看了些原理关系的,但是此时原理关系的物品如同在这里基本没啥用,所以我选择系统性的学习一下,这里我选用的是《jvmG1源码剖析和调优》这本书,在读到5.4节的时刻:
发现有两个之前没有见过的参数,一个是G1LogLevel,一个是UnlockExperimentalVMOptions,从解释说明过去看性能了之后能失掉到愈加详细的YGC日志,于是加上了这个参数而后继续观察,日志格局太长,只截取了局部日志消息,有兴味的可以上去自己打印一下:
可以发如今SystemDictionaryRoots阶段是比拟慢的,但是这个又是啥玩意呢?在书外面是没有任何引见的,于是又启动少量谷歌,终于是找到了一篇你假笨写的一篇文章:JVM源码剖析之自定义类加载器如何拉长YGC,剧烈介绍大家读完这篇文章。
好了最后我来盘一盘究竟为什么会出现gc慢的疑问呢?咱们这个定时义务是一个定时查问微信退款消息的,微信的退款消息要求解析XML,就有如下代码:
而咱们的罪魁祸首其实就在这个new XStream这个方法中,咱们的自动结构方法会调用上方的这个结构方法:
要求留意的是咱们每次创立一个XStream都会新创立一个ClassLoader,先解释一下ClassLoader,这里间接援用你假笨的一段话:
这里着关键说的两个概念是初始类加载器和定义类加载器。举个栗子说吧,AClassLoader->BClassLoader->CClassLoader,示意AClassLoader在加载类的时刻会委托BClassLoader类加载器来加载,BClassLoader加载类的时刻会委托CClassLoader来加载,假设咱们经常使用AClassLoader来加载X这个类,而X这个类最终是被CClassLoader来加载的,那么咱们称CClassLoader为X类的定义类加载器,而AClassLoader为X类的初始类加载器,JVM在加载某个类的时刻对AClassLoader和CClassLoader启动记载,记载的数据结构是一个叫做SystemDictionary的hashtable,其key是依据ClassLoader对象和类名算进去的hash值(其实是一个entry,可以依据这个hash值找到详细的index位置,而后构建一个蕴含kalssName和classloader对象的entry放到map里),而value是真正的由定义类加载器加载的Klass对象,由于初始类加载器和定义类加载器是不同的classloader,因此算进去的hash值也是不同的,因此在SystemDictionary里会有多项值的value都是指向同一个Klass对象。
咱们把这个放到咱们的场景来看就是上方这个状况:
由于咱们每次恳求都会新创立一个Xstream对象,从而也会新创立一个ClassLoader,由于咱们的ClassLoader的key是依据每个对象来算进去的hash值,假设每次都新创立,人造hash值不一样,从而造成咱们有很多ClassLoader指向XStream这个class。为什么SystemDictionary的大小会影响咱们GC时期呢?
构想一下这么个状况,咱们加载了一个类,而后构建了一个对象(这个对象在eden里构建)当一个属性设置到这个类(static变量)里,假设gc出现的时刻,这个对象是不是要被找进去标活才行,那么人造而然咱们加载的类必需是咱们一项关键的gcroot,这样SystemDictionary就成为了gc环节中的被扫描对象了。
咱们的class消息是被调配在哪里的呢?在java7的话是在终身代,在java8就到来了元数据空间也就是咱们的堆上,所以咱们的younggc的时刻是不会回收咱们的class消息的,那么咱们怎样处置这个疑问呢?
但是咱们这个疑问不应该经过渣滓搜集去处置,而是应该从根源上去处置,那就是不能经常使用自动的XStream结构函数,而是要求经常使用固定ClassLoader的结构函数。
经过修正之后上线,经过观察,没有出现慢GC的现象。
最后
经过这次排查的阅从来看,遇到GC疑问尤其是那种比拟不经常出现的,真的是十分难搞,你或许要求对这个疑问启动系统的学习,以及少量的查找资料能力找到要素,我在排查这个疑问的时刻掉了不少头发。在这里记载一下这个阅历,宿愿对大家的一些排查能有协助。