FastJson使用不当导致内存泄漏排查及解决过程 | Java Debug 笔记

6,520 阅读6分钟

本文正在参加「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配置信息,如下图

微信图片_20210510205733.png

如图所示,堆内存配置了5G,其中

年轻代:Eden区(1879M) + S0(85M) + S1(84M) = 2048M

老年代:3072M

垃圾收集器用的是Java8默认的注重吞吐量的并行收集器。

可以确认,一般来讲这个堆大小一定是够用的。

3、服务内存使用情况

微信图片_20210510204939.png 一个稳定的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数组占用了绝大部分堆内存。如下图

2021051001.png

到此为止,已发现两个关键线索:fastJson、IdentityHashMap。

不了解这俩线索没关系~,第一时间当然是在谷歌里搜索:fastJson IdentityHashMap

2021051002.png

你会发现有很多内存泄漏相关的帖子,真相逐渐清晰~

通过其他博客及源码调用链发现如下

  // 可能是为了性能,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再联想上个迭代相关改动,定位出相关代码,如下图

WechatIMG49.png

解释:这是一个工具类,专门用于将Json结构的String串转换成带有泛型类型的对象。

比如将json串转换成Response<List<UserInfo>>对象。

结合上述“fastJson的parseObject方法中的IdentityHashMap缓存”,内存泄漏原因归纳如下

  1. 每调用一次该工具类,都会实例化ParameterizedTypeImpl对象
  2. JSONObject.parseObject(json, type)方法会将type对象(即ParameterizedTypeImpl对象)put到IdentityHashMap缓存中
  3. 又因为IdentityHashMap,是一个线性探测模式的Hash表,他的key是由System.identityHashCode(key)生成,可以理解为它就是一个大数组,每实例化一个type对象都会被放到这个数组中
  4. 所以随着这个工具类的不断调用,IdentityHashMap缓存数组越来越大,而不会被GC,直到堆内存被打满

四、问题解决

  1. 删除触发内存泄漏的工具类
  2. 针对json串转换成带泛型对象,单独实现。 参考了github.com/alibaba/fas… TypeReference的用法

WechatIMG50.png

修改后内存使用率恢复正常

五、问题总结

  1. 这是一次fastJson使用不当导致的内存泄漏
  2. 很多问题往往上升到一定调用量才会暴露出来
  3. 使用第三方工具时还需谨慎,最好多调研他人的使用经验

六、参考资料

【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…