fastjson內存泄漏解決辦法 - springboot實戰電商項目mall4j

springboot實戰電商項目mall4j (https://gitee.com/gz-yami/mall4j)java

java開源商城系統linux

環境:jdk1.8git

系統:window/linuxspring

fastjson版本:1.2.29json

關鍵代碼:springboot

public class FastJsonUtil {
    /* * 將 pojo 對象轉爲 json 字符串,而且駝峯命名修改成下劃線命名 */
    public static String buildData(Object bean) {
        try {
            SerializeConfig config = new SerializeConfig();
            config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;
            return JSON.toJSONString(bean, config);
        } catch (Exception e) {
            return null;
        }
    }
}
複製代碼
// 隨意的一個實體類
public class T {
    public String userName;
    public String userArg;
    public String userGender;
    public String userAddr;

    public T(String userName, String userArg, String userGender, String userAddr) {
        this.userName = userName;
        this.userArg = userArg;
        this.userGender = userGender;
        this.userAddr = userAddr;
    }
}
複製代碼

模擬併發 (也能夠使用 JMeter)markdown

public class Main {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(6, 6, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(999999));
        for (;;) {
            // 每隔1毫秒提交一次任務
            TimeUnit.MILLISECONDS.sleep(1);
            poolExecutor.submit(Main::prcess);
        }
    }

    // 模擬一個請求調用,最終將一個 t 對象轉爲 json 格式返回
    public static void prcess() {
        T t = new T("1", "2", "3", "4");
        String res = FastJsonUtil.buildData(t);
        // System.out.printf(res);
    }
}
複製代碼

jvm 啓動參數:併發

-Xms4g -Xmx4g -Xmn2g -Xss1024K -XX:+UseConcMarkSweepGC(限制堆內存大小、新時代區與老年代的比例調節爲1:一、設置線程棧大小、以及使用 cms垃圾回收器)框架

運行一段時間,會發現該 java 程序佔用的內存遠遠大於 4g。jvm

因爲已經設置的堆內存大小已經限制爲 4g 之內,然而 程序佔用內存遠超過 4g,直接往物理內存攀升,那麼多是堆外內存(直接內存或者是 metaspace 的內存)的泄漏致使的。

在該程序代碼中,並無對直接內存的操做,沒有使用 netty 等與 io 相關的框架。

爲了更加精肯定位是直接內存仍是 metaspace,追加一個啓動參數 -XX:MaxDirectMemorySize=1g,重啓項目,發現沒有任何效果,問題依舊存在。

因此將問題鎖定到 matespace 上,使用 jmap -heap pid 查看堆使用狀況:

能夠發現其中的 MaxMetaspaceSize 爲系統內存大小,沒有收到任何限制。因此多是由於調用 fastjson 的某個方法後,它處理了一些某些事情,須要將某些東西存到 metaspace 中,因爲 metaspace 沒有限制內存大小,致使 java 程序佔用內存狀況超過 4g,不斷攀升,最終會引起內存預警。

嘗試着在 jvm 啓動參數加上 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m(通常來講這兩個值能夠設置爲同樣,再也不贅述),再次啓動項目,模擬併發,發現佔用內存狀況一切正常,沒有超過限制值的狀況出現。

因此,到這一步,能夠判斷出是因爲 fastjson 在處理的時候可能一直加載了某個 class,致使 metaspace 內存佔用過大。

在啓動參數加上 -verbose:class 後再次啓動項目,觀察重複加載了哪一個 class

[Loaded com.alibaba.fastjson.serializer.ASMSerializer_1_T from file:/E:/maven/repository/com/alibaba/fastjson/1.2.29/fastjson-1.2.29.jar]
[Loaded com.alibaba.fastjson.serializer.ASMSerializer_1_T from file:/E:/maven/repository/com/alibaba/fastjson/1.2.29/fastjson-1.2.29.jar]
[Loaded com.alibaba.fastjson.serializer.ASMSerializer_1_T from file:/E:/maven/repository/com/alibaba/fastjson/1.2.29/fastjson-1.2.29.jar]
[Loaded com.alibaba.fastjson.serializer.ASMSerializer_1_T from file:/E:/maven/repository/com/alibaba/fastjson/1.2.29/fastjson-1.2.29.jar]
...
複製代碼

綜上,顯然能夠獲得結論,一直在加載的是 ASMSerializer_1_T 這個 class。

接下來就是查找 fastjson 在哪裏一直重複加載了這個 class。

從 JSON.toJSONString 方法開始入手,進入到 com.alibaba.fastjson.JSON#toJSONString(java.lang.Object, com.alibaba.fastjson.serializer.SerializeConfig, com.alibaba.fastjson.serializer.SerializeFilter[], java.lang.String, int, com.alibaba.fastjson.serializer.SerializerFeature...) 方法中

再依次進入 getObjectWriter(clazz) -> config.getObjectWriter(clazz) -> put(clazz, createJavaBeanSerializer(clazz)) -> createJavaBeanSerializer(beanInfo) -> createASMSerializer(beanInfo) 方法中

在 createASMSerializer 中,有這樣幾行代碼

......
// 拼接類名
String className = "ASMSerializer_" + seed.incrementAndGet() + "_" + clazz.getSimpleName();
String packageName = ASMSerializerFactory.class.getPackage().getName();
String classNameType = packageName.replace('.', '/') + "/" + className;
String classNameFull = packageName + "." + className;
ClassWriter cw = new ClassWriter();
// 而後這裏就加載了 ASMSerializer_ 的類
cw.visit(V1_5 //
         , ACC_PUBLIC + ACC_SUPER //
         , classNameType //
         , JavaBeanSerializer //
         , new String[] { ObjectSerializer } //
);
......
複製代碼

因此,就是這裏,每次調用到這裏,就會 load ASMSerializer_1_T 到 metaspace 中。

而這部分代碼在 ASMSerializerFactory 中。

回到用戶代碼,在 SerializeConfig config = new SerializeConfig() 這一行,進入 SerializeConfig 的無參構造,會調用它的有參構造。有參構造中有這麼幾行代碼

if (asm) {
    asmFactory = new ASMSerializerFactory();
}
複製代碼

一切都很清晰了,因爲一直建立 SerializeConfig,致使 ASMSerializerFactory 也會被重複建立,以後 ASMSerializerFactory 再調用 本類中的 createASMSerializer 方法的時候,就會致使重複加載 com.alibaba.fastjson.serializer.ASMSerializer_1_T

解決辦法:

SerializeConfig config = new SerializeConfig();config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase; 這兩行代碼提到方法外面,改造以下:

public class FastJsonUtil {
     private final static SerializeConfig config = new SerializeConfig();
     static {
         config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;
     }
    /* * 將 pojo 對象轉爲 json 字符串,而且駝峯命名修改成下劃線命名 */
    public static String buildData(Object bean) {
        try {
            return JSON.toJSONString(bean, config);
        } catch (Exception e) {
            return null;
        }
    }
}
複製代碼

重啓項目,問題解決,不會再一直加載 ASMSerializer_1_T 這個類了。

使用 JVisualVm 觀察內存活動狀況

修改前的 metaspace:

1620449434760.png

1620449465608.png

修改後的 metaspace:

1620449950717.png

1620450044378.png

明顯能夠觀察到修改後,再也不頻繁裝載 class 、metaspace 內存狀況再也不急劇攀升。

springboot實戰電商項目mall4j (https://gitee.com/gz-yami/mall4j)

java開源商城系統

相關文章
相關標籤/搜索