当前位置: 首页 > 科技观察

记住一个困难的GC故障排除!

时间:2023-03-20 00:55:45 科技观察

本文转载自微信公众号“咖啡拿铁”,作者咖啡拿铁。转载请联系CaféLatte公众号。后台gc问题一直是比较难排查的问题,但是在我们的开发业务中又是经常出现的问题。这不,最近我的项目出现了一个奇怪的gc问题,排查过程比较繁琐,所以把整个排查过程分享在这里,希望对大家有所帮助。早上突然报警,发现ZK断开了链接。从图片上看,我们的错误是断断续续出现的。一开始我们以为是ZK有问题,后来检查了其他服务,ZK没有问题。于是怀疑是内码有问题导致的。经过研究,发现是zk心跳超时导致的,所以怀疑有两种情况:显然,其他服务都没有问题,应该不是抖动造成的。因此,机器应间歇性卡住。一般首先出现的是我们的CPU满了,导致机器卡顿。发现是CPU没有问题,然后我们的gc带来的STW会导致我们的jvm进程卡死。经过观察,确实是younggc很慢,导致我们的JVM出现了GCfreeze,所以才会出现这种现象。排查GC问题的原因一般来说两个法宝可以解决大部分问题:GC日志转储文件出现问题后,我立即打开GC日志,截图如下:可以发现我们的younggc已经到了2.7s,大家都知道我们的younggc一路都是STW,也就是说每次gc都会freeze2.7s,那么zk超时就断开链接是正常的。再看gc回收情况,每次都能完全回收。日志中可以明显看出root扫描时间比较长。我当时对这个阶段并不熟悉(后面会继续讲),所以一直不明白为什么会这样。网上各种搜索都没有结论。这时候看到why哥公众号的一篇文章:https://mp.weixin.qq.com/s/KDUccdLALWdjNBrFjVR74Q,建议大家看看这篇文章,这篇文章主要讲的是我们的一个jvm优化,大家知道我们进入STW需要一个安全点,而询问是否进入安全点是需要资源的,所以jvm在做jit优化的时候会讲countedloop,也就是countingloop优化成wholecycle之后结束,进入安全点。相关问题在小米的技术文章:《HBase实战:记一次Safepoint导致长时间STW的踩坑之旅》中也有提到。看了这两篇文章,突然想到我们的代码也是countedloop的形式,所以怀疑也可能是这个问题导致的。马上优化代码,把for(inti=0;iBClassLoader->CClassLoader表示AClassLoader加载类时委托BClassLoader类加载器加载,BClassLoader加载类时委托CClassLoader加载类。如果我们使用AClassLoader来加载X类,而X类最终是由CClassLoader加载的,那么我们称CClassLoader为X类的定义类加载器,AClassLoader是X类的初始类加载器,JVM执行AClassLoader和CClassLoader时加载一个类Record,记录的数据结构是一个hashtable,叫做SystemDictionary,它的key是根据ClassLoader对象和类名计算出来的hash值(其实就是一个entry,可以根据索引找到具体的索引位置)hash值,然后构建一个包含kalssName和classloader对象的入口的list放在map中),value就是定义类加载器加载的真正的Klass对象,因为初始类加载器和定义类加载器是不同的classloader,所以计算出来的hash值也是不同的。因此,SystemDictionary中会有多个值都指向同一个Klass对象。我们把这个放到我们的场景中看看下面的情况:由于我们每次请求都会创建一个新的Xstream对象,所以也会创建一个新的ClassLoader,因为我们的ClassLoader的key是根据每个对象计算的hash值,如果每次都是新创建的,自然hash值不同,所以我们有很多ClassLoader指向XStream类。为什么SystemDictionary的大小会影响我们的GC时间?想象这样一种情况,我们加载一个类,然后构建一个对象(这个对象是在eden中构建的)。当给这个类(静态变量)设置了一个属性时,如果发生gc时,是否应该找到这个对象并标记为alive?那么自然我们加载的类肯定是??我们重要的gc根,所以SystemDictionary就成为了gc过程中的扫描对象。我们的班级信息分配在哪里?在java7中,是在永久代中。在java8中,涉及到了元数据空间,也就是我们的堆,所以我们的younggc是不会回收我们的类信息的,那么我们怎么解决这个问题呢?java7:在G1垃圾收集器中,永久代只有在fullGC的时候才会被回收,这个过程是stop-the-world。当不进行FullGC时,G1以最佳方式运行。只有当永久代已满或者应用程序分配内存的速度超过G1收集垃圾的速度时,G1才会触发FullGC。在CMS垃圾回收器中,我们可以使用-XX:+CMSClassUnloadingEnabled来回收CMS并发周期中的永久代。G1中没有相应的设置。G1只有在停止时才会回收永久代——the-world的FullGC。我们可以根据应用的需要,通过设置PermSize和MaxPermSize参数来调整永久代的大小。java8:提供了四个参数-XX:MetaspaceSize、-XX:MaxMetaspaceSize、-XX:MinMetaspaceFreeRatio、-XX:MaxMetaspaceFreeRatio,用于控制元空间的大小,超过比例或大小时将被收集。但是我们的问题不应该通过垃圾回收来解决,而应该从根本上解决,即我们不能使用默认的XStream构造函数,而是需要使用固定的ClassLoader构造函数。修改后上线。经过观察,并没有GC慢的现象。最后,根据这次调查的经验,确实很难遇到GC问题,尤其是那些比较少见的问题。您可能需要系统地研究这个问题并查找大量资料才能找到原因。在解决此问题时,我掉了不少头发。记录下这段经历,希望对以后的一些调查有所帮助。