一次诡异的内存泄露排查过程,背后原因令人深思
Posted 软件测试君
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一次诡异的内存泄露排查过程,背后原因令人深思相关的知识,希望对你有一定的参考价值。
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,造成应用程序性能的下降。
技术行业要不断地学习,学习肯定不要孤军奋战,最好是能抱团取暖,相互成就一起成长,群众效应的效果是非常强大的,大家一起学习,一起打卡,会更有学习动力,也更能坚持下去。你可以加入我们的测试技术交流扣扣群:914172719(里面有各种软件测试资源和技术讨论)
送给大家一句话,共勉:当我们能力不足的时候,首先要做的是内修!当我们能力足够强大的时候,就可以外寻了!
最后也为大家准备了一份配套的学习资源,你能在 公众号:【伤心的辣条】免费获取一份216页软件测试工程师面试宝典文档资料。以及相对应的视频学习教程免费分享!,其中资料包括了有基础知识、Linux必备、Shell、互联网程序原理、mysql数据库、抓包工具专题、接口测试工具、测试进阶-Python编程、Web自动化测试、APP自动化测试、接口自动化测试、测试高级持续集成、测试架构开发测试框架、性能测试、安全测试等。
好文推荐
转行面试,跳槽面试,软件测试人员都必须知道的这几种面试技巧!
面试官:工作三年,还来面初级测试?恐怕你的软件测试工程师的头衔要加双引号…
以上是关于一次诡异的内存泄露排查过程,背后原因令人深思的主要内容,如果未能解决你的问题,请参考以下文章