記一次Orika使用不當導致的內存溢出

hprof 文件分析

2021-08-24,訂單中心的一個項目出現了 OOM 異常,使用 MemoryAnalyzer 打開 dump 出來的 hprof 文件,可以看到 91.27% 的內存被一個超大對象javassist.ClassPool佔用了。

那麼,ClassPool是一個什麼樣的對象呢?我們知道,javassist 可以用來動態生成類,而生成的類就是放在這個ClassPool裏面,具體以javassist.CtClass的形式存在。

所以,初步分析是 OOM 的原因是 javassist 生成的CtClass對象過多,即 javassist 生成了太多的類

bug_analysis_001

爲了驗證我的猜想,我需要看看CtClass對象的內存情況,點擊 Actions -> Histogram,如圖。果然,這 2.3 G 的內存就是CtClass對象佔用的。

bug_analysis_002

接下來,我需要知道這些CtClass對象都是哪些類,點擊 List objects -> with outgoing references。這時可以看到,項目裏生成了大量的Orika_ProductionOrderUpdateCmd_ProductionOrderE_Mapper*

看着這些類的命名規則,是不是很熟悉呢?它們都是 orika 映射 bean 時動態生成的類。所以,大量的CtClass對象是由 orika 產生。orika 的原理我之前講過(cglib、orika、spring等bean copy工具性能測試和原理分析),這裏就不再贅述。

bug_analysis_003

但是,orika 生成的映射類是可以複用的,爲什麼還會有這麼多重複的映射類呢?

項目代碼分析

在項目中找到唯一一處將ProductionOrderE映射成ProductionOrderUpdateCmd的地方。

bug_analysis_004

在項目中,其他地方都是使用方法 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 時,生成了兩個映射類。

bug_analysis_005

以下是方法 2 的底層封裝,這裏使用ClassMapBuilder重新配置了ProductionOrderUpdateCmdProductionOrderE的映射關係,導致上一次 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
    }

解決方案

經過上面的分析,解決方案就呼之欲出了,我們只需要在初始化時一次定義好ProductionOrderUpdateCmdProductionOrderE的映射關係就行了,如下。當然,方法 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章