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)併發
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垃圾回收器)jvm
運行一段時間,會發現該 java 程序佔用的內存遠遠大於 4g。maven
因爲已經設置的堆內存大小已經限制爲 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:
修改後的 metaspace:
明顯能夠觀察到修改後,再也不頻繁裝載 class 、metaspace 內存狀況再也不急劇攀升。