美文网首页
com.alibaba.fastjson存在内存泄漏

com.alibaba.fastjson存在内存泄漏

作者: 书唐瑞 | 来源:发表于2021-12-31 21:39 被阅读0次

    [背景]

    在这里插入图片描述

    发现线上机器的元空间在增长, 发生了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,选择withincomingreferences,就可以查到到哪些引用了这个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.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新年快乐!

    相关文章

      网友评论

          本文标题:com.alibaba.fastjson存在内存泄漏

          本文链接:https://www.haomeiwen.com/subject/mnenqrtx.html