本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看活动链接
本文主要分享之前线上遇到的一个内存泄漏的典型案例,方便大家避坑,以及遇到内存泄漏相关问题时相应的排查思路。
所有截图及内容均已脱敏,涉及相关系统和接口均描述为A系统服务,B接口。
一、问题背景
5月的某一天20:40左右手机突然收到告警:A服务内存使用率超过75%,恰好有研发同学值班,监控发现B接口性能出现极大值。
问题同步后,查看接口日志未发现明显异常,紧接着查看JVM监控,发现在20:40触发了一次Full GC,并且堆内存几乎打满,而这次Full GC执行时长居然长达十几秒,可以说这次Full GC执行得很不顺利,故接口性能出现极值是Full GC导致。
问题暴露并初步定位是堆内存异常后,为不影响接口性能及线上服务,先将容器重启了,堆内存被重置,接口性能及内存恢复正常。
二、问题观察跟进
我们需要排查出导致堆内存异常打满的原因。
1、A服务背景
A服务需求较少,没有频繁的迭代上线,上次上线还是2个月前。
此次内存告警是该服务正式上线以来首次发生。
同时也是上次迭代上线后跑了将近2个月后发生的问题,那该问题极有可能跟上次上线代码有关。
2、A服务JVM配置
查询JVM配置
第一步,执行:jcmd,找到java服务的进程ID
第二步,执行:jmap -heap pid,得到JVM配置信息,如下图
如图所示,堆内存配置了5G,其中
年轻代:Eden区(1879M) + S0(85M) + S1(84M) = 2048M
老年代:3072M
垃圾收集器用的是Java8默认的注重吞吐量的并行收集器。
可以确认,一般来讲这个堆大小一定是够用的。
3、服务内存使用情况
一个稳定的Java应用,由于Java自己的垃圾回收机制(Young GC + Full Gc),即使运行很长时间,堆内存应该处于一个稳定的区间内,内存使用率同样也应该处于一个稳定的区间内。
但如图发现,随着时间的推移,内存使用率正逐步递增,确丝毫没有递减的趋势。
另外查看这段时间内系统Young GC监控情况,发现:随着时间的推移Yong GC后剩余的堆内存越来越大,即使发生了Full GC,堆内存依然越来越大。
综上,种种迹象表明,A服务大概率存在堆内存泄漏,并且跟上次迭代上线有关!
三、问题定位
既然定位到了Java堆内存泄漏,那么我们就要具体定位是什么对象占用了多堆内存?又是什么程序导致了这么多堆内存?为什么这些堆内存没有被GC回收?
利用MAT工具查看堆内存对象使用情况
第一步,执行:jcmd,还是找到我们的java进程ID
第二步,利用jmap命令,打印dump二进制日志,用于分析堆内存中的对象存储信息
执行:jmap -dump:format=b,file=/logs/dump.log pid
命令执行完毕后,将生成的日志文件下载到本地(一般日志比较大,建议压缩后再下载)
第三步,利用MAT工具查看堆内存使用情况
MAT(Memory Analyzer Tool),是一个java堆内存分析工具,可有效帮助我们排查内存泄漏问题,具体使用细则可自行查询。
前面dump日志有4个G。。。 因日志过大,需要修改MAT的MemoryAnalyzer.ini配置文件:-Xmx5120m,否则无法分析
MAT工具导入dump日志分析后发现:fastJson的IdentityHashMap的Entry数组占用了绝大部分堆内存。如下图
到此为止,已发现两个关键线索:fastJson、IdentityHashMap。
不了解这俩线索没关系~,第一时间当然是在谷歌里搜索:fastJson IdentityHashMap
你会发现有很多内存泄漏相关的帖子,真相逐渐清晰~
通过其他博客及源码调用链发现如下
// 可能是为了性能,fastJson的parseObject方法中
// 会对泛型中的类生成一个ObjectDeserializer(对象反序列化器)对象
// 并缓存到IdentityHashMap容器中
// IdentityHashMap,是一个线性探测模式的Hash表,只不过他的key是由System.identityHashCode(key)生成,
// 即一个对象对应一个唯一key且与对象的HashCode方法不同,无法被重写。
// 在这里它的key是一个Type对象,也就是说只要对象不相同,必然在hash表中缓存不同值
private final IdentityHashMap<Type, ObjectDeserializer> deserializers = new IdentityHashMap<Type, ObjectDeserializer>();
根据fastJson再联想上个迭代相关改动,定位出相关代码,如下图
解释:这是一个工具类,专门用于将Json结构的String串转换成带有泛型类型的对象。
比如将json串转换成Response<List<UserInfo>>对象。
结合上述“fastJson的parseObject方法中的IdentityHashMap缓存”,内存泄漏原因归纳如下
- 每调用一次该工具类,都会实例化ParameterizedTypeImpl对象
- JSONObject.parseObject(json, type)方法会将type对象(即ParameterizedTypeImpl对象)put到IdentityHashMap缓存中
- 又因为IdentityHashMap,是一个线性探测模式的Hash表,他的key是由System.identityHashCode(key)生成,可以理解为它就是一个大数组,每实例化一个type对象都会被放到这个数组中
- 所以随着这个工具类的不断调用,IdentityHashMap缓存数组越来越大,而不会被GC,直到堆内存被打满
四、问题解决
- 删除触发内存泄漏的工具类
- 针对json串转换成带泛型对象,单独实现。 参考了github.com/alibaba/fas… TypeReference的用法
修改后内存使用率恢复正常
五、问题总结
- 这是一次fastJson使用不当导致的内存泄漏
- 很多问题往往上升到一定调用量才会暴露出来
- 使用第三方工具时还需谨慎,最好多调研他人的使用经验
六、参考资料
【MAT工具使用介绍】blog.csdn.net/bohu83/arti…
【fastjson反序列化使用不当导致内存泄露】 www.cnblogs.com/liqipeng/p/…
【“com.alibaba.fastjson”遇到的内存泄漏问题】www.jianshu.com/p/adfde1a31…
【GitHub-Issue:parseObject是否存在内存泄漏情况】github.com/alibaba/fas…
【GitHub-Wiki:TypeReference使用】github.com/alibaba/fas…