一次诡异的内存泄露排查过程,背后原因令人深思

01 起因

前几天,群里一个学员发消息,说在压测时发现某应用程序jvm中FullGC特别多,不知道正常不正常,并把gc的截图发了出来。点开一看是这个样子
在这里插入图片描述

这是 jstat监控 的截图,FGC那列代表数字代表从应用启动开始到目前为止FullGC发生的次数,数据是每秒打印一行,所以从图里可以看出,FullGC次数从2735到2736,只用了10秒,也就是大概10秒就会发生一次FullGC,这种频率对于一个Java程序来说太过于频繁了。每次FullGC都会造成应用程序的暂停,从而引起响应时间边长,程序性能也严重下降。从图里也能印证这一点,每次FullGC花费的时间在400ms多。

02 分析

图里也有一个奇怪的现象,一般发生FullGC,都是因为jvm中老年代空间不足引起的,图中第4列就是老年代的数据,图里可以看到,老年代只占用了3%左右,远远达不到上限100%。

学员表示在这种情况下,用10并发去压测接口,tps大概900多,看起来性能还可以,但是FullGC这么频繁肯定是有问题,还是需要排查下的。

也没考虑太多,既然FullGC这么频繁,那就看看jvm中对象的分配情况吧,于是让同学用 jmap 去打印下堆内存中的对象信息,结果如下:
在这里插入图片描述

前4行分别是jdk里自带的数据对象,char[]、byte[]、int[]和String对象,这种一般都是正常的,看不出什么问题,可以忽略。

第5行和6行根据类名可以看出是fastjson的对象,fastjson主要用于json转换,是Java中常用的一种json序列化组件。除此之外,其他的对象都是jdk自带的,没有跟业务相关的对象。因此可以猜测,大概率是因为fastjson组件引起的。

又看了下学员提供的其他截图,发现应用程序jvm参数那里配的不太合理

没有配新生代的大小,这样jvm会使用默认值

永久代参数配置有误,从jdk8起,永久代的参数已经从PermSize改成了MataSpace
在这里插入图片描述

于是先让学员把参数都改下,毕竟排查问题的基础是参数配置合理,也没准就是因为参数的问题导致的呢。

学员改完后又重新压测了下,这次FullGC的频率下降了很多,大概30秒FullGC一次。JVM的回收也很规律。

在这里插入图片描述

貌似看着参数修改是起了一定的效果,但是学员表示现在tps降低了,jmeter显示tps大概在100-200之间,比刚才修改前的900要低很多

在这里插入图片描述

这就尴尬了,越调tps越低了,于是又把目光转到刚才的fastjson上。学员正好有服务端代码查看权限,于是就让学员看看代码中哪些地方用到了fastjson。

学员说这个接口的逻辑非常简单,就是从数据库里查询用户信息,然后将数据转换为json字符串返回:
在这里插入图片描述

在handle方法中,果然用到了fastjson
在这里插入图片描述

这块代码写的比较简单,貌似看也没啥问题。为了印证确实是handle函数的问题,使用模块隔离法进行验证下。让学员跟开发反馈下,不要使用handle函数处理了,直接返回一个写死的json,然后验证下。

修改代码后,用jmeter又压了一次,接口的tps能到1400+,并且FullGC也很正常,这就说明是确实是handle函数代码有问题引起的。

将代码恢复原样,再次进行压测排查时,学员又发来另外一个jvm的监控截图,说metaspace空间的波动曲线和类加载的波动曲线比较一致,会不会是代码中有对象实例生成到metaspace中了,并且没有释放掉
在这里插入图片描述

当看到这张图之后,恍然大悟。上升的曲线说明metaspace中在不断的加载类,且加载到接近上限时,会触发一次回收。释放了部分类,会造成一个波谷。学员也观察到metaspace波谷出现的频率和FullGC频率一致,那说明是metaspace回收引起的FullGC。

这种情况确实也比较少见。在这里跟大家解释下,metaspace是jvm中的一个内存空间,里面主要存放了类的基本信息、静态变量、常量等。一般来说,在java程序运行过程中,每个类第一次创建的对象时,会把类信息加载到metaspace中,且只加载一次,后续无论多少并发和请求,类不会重复加载的。因此metaspace的空间使用是非常稳定的,基本上不会随着并发的变化而变化。在多数的压测过程中,可以看到mataspace的内存占用是一条直线。只要在应用启动的时候,给metaspace一个比较大的初始空间,是不会造成FullGC的。

当然了,有一种特殊情况除外。如果代码中存在动态加载类的情况,那每个线程在执行代码时,都会重新加载类到metaspace中,通常在使用反射的场景中用的比较多。

目光又回到代码中的handle方法,在此方法中,果真是有一行动态加载类的代码
在这里插入图片描述

在创建了config对象后,会put一个Long.class,这个时候就会把Long这个类加载到metaspace中,而且handle方法是每次请求都会调用。所以metaspace空间才增长这么快。当然了,单纯每次创建对象,并不会造成内存溢出,这就是为什么老年代的使用量并不是很高的原因。问题还是出在了每次都加载了Long.class

在代码中,config对象是一个配置对象,其实并不需要每次调接口都创建一个对象,然后又加载一次配置。可以做成全局的静态变量,然后加载一次配置即可。

代码修改如下,定义为静态变量,然后在静态代码块中进行初始化
在这里插入图片描述

使用jmeter重新进行验证,在10并发下,tps现在能跑到1400左右,并且没有出现FullGC了。此问题得到了解决,而且因为handle是项目里的一个公共方法,此问题解决会将项目里的所有接口性能提升一个台阶。

03 总结

事后回想起来,其实这个问题本应该早就被发现的,因为在最早的gc截图里,已经能看出来是metaspace造成了FullGC,只不过当时没太关注metaspace,再加上文本式的打印不如曲线图那么直接
在这里插入图片描述

这也给所有使用fastjson的同学提个醒,在使用序列化配置功能的时候,切记配置对象要定义成全局静态的,否则就会造成元空间内存溢出,从而触发FullGC,造成应用程序性能的下降。

猜你喜欢

转载自blog.csdn.net/Testfan_zhou/article/details/124037430
今日推荐