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:
修改後的 metaspace:
明顯能夠觀察到修改後,再也不頻繁裝載 class 、metaspace 內存狀況再也不急劇攀升。