com.alibaba.fastjson存在内存泄漏

Netty历险记

共 2447字,需浏览 5分钟

 ·

2022-01-01 05:23

[背景]



发现线上机器的元空间在增长, 发生了FGC. 

由于拿不到线上机器的dump文件, 于是乎, 在预发环境, 执行jmap命令, 得到dump文件.


使用MemoryAnalyzer分析dump文件.



如上图, 在查看线程信息的时候, 发现Dubbo线程, MQ线程, xxl-job线程这些线程, 它们`持有`上百KB的内存. 常规情况, 线程不会`持有`这么大的内存.


拿其中一个Dubbo线程, 查看下它内部的属性




如上图, 在线程的ThreadLocalMap中存在197.05KB的数据


查看ThreadLocalMap中的信息



如上图, 在ThreadLocalMap的12号位置, 存储了128.02KB的字符数组. 里面存储的都是业务信息. 


那么是由哪个ThreadLocal放到这个线程的ThreadLocalMap中的呢?


往下看



如上图,在ThreadLocalMap中, ThreadLocal作为Key, 于是右击图中的ThreadLocal, 选择`with incoming references`, 就可以查到到哪些引用了这个ThreadLocal.



如上图, 发现com.alibaba.fastjson.JSON引用了ThreadLocal.


根据这个线索, 查看了下业务代码.




在业务代码中, 使用了

com.alibaba.fastjson.JSON#parseObject()

跟进这个方法



有一个allocateChars方法

fastjson先从当前线程中得到char[],如果没有则创建一个char[], 并放入到线程的ThreadLocalMap中.

这也是fastjson为了提高性能的一个手段. 但是它却造成了内存泄漏. 因为没有任何地方调用了remove()方法.


排查到这里后, 我去GitHub上查看了下, 原来在今年(2021年)5月份已经有人在GitHub上提出了这个问题.



地址: https://github.com/alibaba/fastjson/issues/3751


我也在下方贴出了我的案例(也就是本文所说的)




但是, 似乎这个问题官方还没有给出一个比较好的解决方案. (master代码和最新的1.2.79版本均没有看到解决它的`身影`)



目前有2个解决方案.

第一个方案

Field charsLocal = JSON.class.getDeclaredField("charsLocal");charsLocal.setAccessible(true);
if (charsLocal.get(null) instanceof ThreadLocal) { ThreadLocal threadLocal = (ThreadLocal) charsLocal.get(null); threadLocal.remove();}

通过反射的方式, 拿到charsLocal属性, 主动调用它的remove()方法.

但这种方案并不是最好的方案. 为了提高性能, 不得不把一些事先创建好的char[] 放入到线程的ThreadLocalMap中, 但是如果放入的太多又会造成内存泄漏太多.   既不能避免内存泄漏, 又不能泄漏太多, 就是下面的第二个方案.


第二个方案

设定char[]数组的最大长度=128, 假如程序使用了超过128大小的内存, 那么会自动将char[]长度降到128大小, 保证char[]数组的长度不会超过128, 做到可控.

Log4j作为一个日志框架, 在它的低版本中, 也存在大量内存泄漏, 也是因为ThreadLoal的原因. 作为日志框架,必然要使用ThreadLocal来提高性能. 但是在Log4j的高版本中, 针对大量内存泄漏的情况, 做了优化, 超过最大值,就进行缩容. 也就是按照我们这里说的第二个方案. 部分源码如下


//源码类 org.apache.logging.log4j.message.ParameterizedMessagepublic String getFormattedMessage() {    if (this.formattedMessage == null) {        StringBuilder buffer = getThreadLocalStringBuilder();        this.formatTo(buffer);        this.formattedMessage = buffer.toString();        // 进行缩容        StringBuilders.trimToMaxSize(buffer, Constants.MAX_REUSABLE_MESSAGE_SIZE);    }    return this.formattedMessage;}
public static void trimToMaxSize(StringBuilder stringBuilder, int maxSize) {    // 超过设定的默认最大值, 就进行缩容 if (stringBuilder != null && stringBuilder.capacity() > maxSize) { stringBuilder.setLength(maxSize); stringBuilder.trimToSize(); }
}


个人猜测, fastjson大概率也会采取第二个方案, 或者它不理睬这个内存泄漏, 也不好说.


祝大家2022新年快乐!



浏览 31
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报