com.alibaba.fastjson存在内存泄漏

2022-06-02 14:57:36 浏览数 (1)

[背景]

发现线上机器的元空间在增长, 发生了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.

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

在业务代码中, 使用了

代码语言:javascript复制
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个解决方案.

第一个方案

代码语言:javascript复制
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的高版本中, 针对大量内存泄漏的情况, 做了优化, 超过最大值,就进行缩容. 也就是按照我们这里说的第二个方案. 部分源码如下

代码语言:javascript复制
//源码类 org.apache.logging.log4j.message.ParameterizedMessage
public 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新年快乐!

0 人点赞