hprof 檔案分析
2021-08-24,訂單中心的一個專案出現了 OOM 異常,使用 MemoryAnalyzer 開啟 dump 出來的 hprof 檔案,可以看到 91.27% 的記憶體被一個超大物件javassist.ClassPool
佔用了。
那麼,ClassPool
是一個什麼樣的物件呢?我們知道,javassist 可以用來動態生成類,而生成的類就是放在這個ClassPool
裡面,具體以javassist.CtClass
的形式存在。
所以,初步分析是 OOM 的原因是 javassist 生成的CtClass
物件過多,即 javassist 生成了太多的類。
為了驗證我的猜想,我需要看看CtClass
物件的記憶體情況,點選 Actions -> Histogram,如圖。果然,這 2.3 G 的記憶體就是CtClass
物件佔用的。
接下來,我需要知道這些CtClass
物件都是哪些類,點選 List objects -> with outgoing references。這時可以看到,專案裡生成了大量的Orika_ProductionOrderUpdateCmd_ProductionOrderE_Mapper*
。
看著這些類的命名規則,是不是很熟悉呢?它們都是 orika 對映 bean 時動態生成的類。所以,大量的CtClass
物件是由 orika 產生。orika 的原理我之前講過(cglib、orika、spring等bean copy工具效能測試和原理分析),這裡就不再贅述。
但是,orika 生成的對映類是可以複用的,為什麼還會有這麼多重複的對映類呢?
專案程式碼分析
在專案中找到唯一一處將ProductionOrderE
對映成ProductionOrderUpdateCmd
的地方。
在專案中,其他地方都是使用方法 1,唯獨這裡使用了方法 2,所以,有理由懷疑是不是方法 2 有 bug 呢?
public class BeanUtils {
// 方法1
public static <S, D> D copy(S source, Class<D> destinationClass) {
// ······
}
// 方法2
public static <S, D> D copy(S source, Class<D> destinationClass, String excludeFields) {
// ······
}
}
於是,我寫了個簡單的 demo,如下。我的假設是,使用方法 2 不會複用對映類,每 copy 一次就生成一個對映類,最終導致對映類過多。至於生成了幾個對映類,我們可以通過輸出對映類檔案的方式來判斷,使用啟動引數-Dma.glasnost.orika.GeneratedSourceCode.writeSourceFiles=true -Dma.glasnost.orika.writeSourceFilesToPath=D:/tmp/orika
可以輸出對映類檔案。
public static void main(String[] args) {
ProductionOrderE productionOrder = new ProductionOrderE();
// 使用方法2
ProductionOrderUpdateCmd copy = BeanUtils.copy(productionOrder, ProductionOrderUpdateCmd.class,
"belongShop,belongOrg,userOperate,orgExtendInfo");
ProductionOrderUpdateCmd copy2 = BeanUtils.copy(productionOrder, ProductionOrderUpdateCmd.class,
"belongShop,belongOrg,userOperate,orgExtendInfo");
// 使用方法1
// ProductionOrderUpdateCmd copy3 = BeanUtils.copy(productionOrder, ProductionOrderUpdateCmd.class);
// ProductionOrderUpdateCmd copy4 = BeanUtils.copy(productionOrder, ProductionOrderUpdateCmd.class);
// zzs001
}
執行方法,我們會發現,使用方法 1 時,只生成了一個對映類,而使用方法 2 時,生成了兩個對映類。
以下是方法 2 的底層封裝,這裡使用ClassMapBuilder
重新配置了ProductionOrderUpdateCmd
和ProductionOrderE
的對映關係,導致上一次 copy 時生成的CtNewClass
物件不再複用。
所以,在使用 orika 時,A->B 的對映關係只能定義一次,不能反覆定義。
private MapperFactory mapperFactory;
public <S, D> D copy(S source, Type<S> from, Type<D> to, String excludeFields) {
List<String> list = new ArrayList<>();
if(excludeFields != null) {
list = Arrays.asList(excludeFields.split(","));
}
ClassMapBuilder cb = this.mapperFactory.classMap(from, to);
for(String s : list) {
cb.exclude(s.trim());
}
cb.byDefault().register();
return this.mapperFactory.getMapperFacade().map(source, from, to);
// zzs001
}
解決方案
經過上面的分析,解決方案就呼之欲出了,我們只需要在初始化時一次定義好ProductionOrderUpdateCmd
和ProductionOrderE
的對映關係就行了,如下。當然,方法 2 不能再用了。
public class BeanUtils {
static {
ClassMapBuilder cb = BeanToolkit.instance().getMapperFactory().classMap(
TypeFactory.valueOf(ProductionOrderE.class),
TypeFactory.valueOf(ProductionOrderUpdateCmd.class)
);
cb.exclude("belongShop");
cb.exclude("belongOrg");
cb.exclude("userOperate");
cb.exclude("orgExtendInfo");
cb.byDefault().register();
// zzs001
}
}
結語
經過以上分析,我們找到了 OOM 的原因,並較好地解決了問題。其實,我們應該更早的監控到異常,像上面說的這種會出現非堆記憶體過高的情況。
最後,感謝閱讀,歡迎私信交流。
本文為原創文章,轉載請附上原文出處連結:https://www.cnblogs.com/ZhangZiSheng001/p/15184914.html