com.alibaba.fastjson存在内存泄漏
[背景]
发现线上机器的元空间在增长, 发生了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.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新年快乐!