前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >com.alibaba.fastjson存在内存泄漏

com.alibaba.fastjson存在内存泄漏

作者头像
书唐瑞
发布2022-06-02 14:57:36
3780
发布2022-06-02 14:57:36
举报
文章被收录于专栏:Netty历险记Netty历险记

[背景]

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-12-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Netty历险记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档