com.alibaba.fastjson存在內(nèi)存泄漏
[背景]

發(fā)現(xiàn)線上機器的元空間在增長,?發(fā)生了FGC.?
由于拿不到線上機器的dump文件,?于是乎, 在預(yù)發(fā)環(huán)境,?執(zhí)行jmap命令, 得到dump文件.
使用MemoryAnalyzer分析dump文件.

如上圖, 在查看線程信息的時候,?發(fā)現(xiàn)Dubbo線程,?MQ線程,?xxl-job線程這些線程, 它們`持有`上百KB的內(nèi)存.?常規(guī)情況,?線程不會`持有`這么大的內(nèi)存.
拿其中一個Dubbo線程, 查看下它內(nèi)部的屬性

如上圖, 在線程的ThreadLocalMap中存在197.05KB的數(shù)據(jù)
查看ThreadLocalMap中的信息

如上圖, 在ThreadLocalMap的12號位置,?存儲了128.02KB的字符數(shù)組.?里面存儲的都是業(yè)務(wù)信息.?
那么是由哪個ThreadLocal放到這個線程的ThreadLocalMap中的呢?
往下看

如上圖,在ThreadLocalMap中, ThreadLocal作為Key,?于是右擊圖中的ThreadLocal,?選擇`with incoming references`, 就可以查到到哪些引用了這個ThreadLocal.

如上圖,?發(fā)現(xiàn)com.alibaba.fastjson.JSON引用了ThreadLocal.
根據(jù)這個線索, 查看了下業(yè)務(wù)代碼.

在業(yè)務(wù)代碼中,?使用了
com.alibaba.fastjson.JSON#parseObject()跟進這個方法

有一個allocateChars方法

fastjson先從當(dāng)前線程中得到char[],如果沒有則創(chuàng)建一個char[], 并放入到線程的ThreadLocalMap中.
這也是fastjson為了提高性能的一個手段. 但是它卻造成了內(nèi)存泄漏.?因為沒有任何地方調(diào)用了remove()方法.
排查到這里后, 我去GitHub上查看了下, 原來在今年(2021年)5月份已經(jīng)有人在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屬性, 主動調(diào)用它的remove()方法.
但這種方案并不是最好的方案.?為了提高性能, 不得不把一些事先創(chuàng)建好的char[] 放入到線程的ThreadLocalMap中,?但是如果放入的太多又會造成內(nèi)存泄漏太多.?? 既不能避免內(nèi)存泄漏, 又不能泄漏太多,?就是下面的第二個方案.
第二個方案
設(shè)定char[]數(shù)組的最大長度=128, 假如程序使用了超過128大小的內(nèi)存,?那么會自動將char[]長度降到128大小, 保證char[]數(shù)組的長度不會超過128,?做到可控.
Log4j作為一個日志框架, 在它的低版本中, 也存在大量內(nèi)存泄漏,?也是因為ThreadLoal的原因.?作為日志框架,必然要使用ThreadLocal來提高性能. 但是在Log4j的高版本中, 針對大量內(nèi)存泄漏的情況, 做了優(yōu)化, 超過最大值,就進行縮容.?也就是按照我們這里說的第二個方案.?部分源碼如下
//源碼類?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) {????// 超過設(shè)定的默認最大值, 就進行縮容if (stringBuilder != null && stringBuilder.capacity() > maxSize) {stringBuilder.setLength(maxSize);stringBuilder.trimToSize();}}
個人猜測,?fastjson大概率也會采取第二個方案, 或者它不理睬這個內(nèi)存泄漏,?也不好說.
祝大家2022新年快樂!
