一次線上JVM記憶體異常排查 -- 物件之間的複製

王翱_奧利奧發表於2019-02-14

1、現象

2月11日閘道器在短時間內出現20+的訪問出錯,檢視kibina如下:

image-20190214103709965

根據trace得到具體的堆疊異常,發現都是負載均衡同一個pod均顯示連線異常:

image-20190214103941508

去grafana檢視該pod執行情況,可以發現記憶體突然下降的情況,同時該pod已經達到記憶體上限(8G),當資源申請不到的情況下,該pod可能存在重啟的情況:

image-20190214104227893

檢視ad服務的日誌驗證了當時確實發生了重啟:

image-20190214104504286

此時可以基本定位到因為記憶體問題使得服務重啟,從而閘道器無法負載均衡到該服務例項導致閘道器報錯,接下來需要明確是什麼原因讓該pod的記憶體佔用如此之高。

二、記憶體問題定位

  • 根據grafana的圖表,可以看出堆記憶體比較正常,而非堆記憶體出現了異常(使用率超過100%):

image-20190214110855773

​ 非堆就是JVM留給自己用的,方法區、JVM內部處理或優化所需的記憶體(如JIT編譯後的程式碼快取)、每個類結構(如執行時常數池、欄位和方法資料)以及方法和構造方法 的程式碼都在非堆記憶體中。

  • 用JProfiler開啟該檔案(也可以用jVisualVM),找到Biggest Objects,然後發現在存活物件中存在大量的AdDto這個類的例項,大量這些物件整整佔用了1G多:

    image-20190214113256395

    image-20190214153409092

    三、程式碼檢查

回到程式碼中,檢查AdDto的生成方式,發現用瞭如下的操作:

image-20190214113956636

modelMapper每一次呼叫addMapping都將建立一份該類的結構(通過位元組碼然後由類載入器載入),查閱官網相關文件。如果轉換型別確定,應該將ModelMapper設定成單例(modelmapper.org/user-manual…

image-20190214141441952

四、線下重現

在普通介面中用這段程式碼驗證猜想,可以很明顯看到非堆記憶體一路猛漲,並且載入類的數量也在一路上升,基本證實了問題:

image-20190214145841931

image-20190214145826877

image-20190214145906502

五、修復並驗證

將程式碼改成如下形式並重新壓測:

    private static ModelMapper modelMapper;

    static {
        modelMapper = new ModelMapper();
        TypeMap<Person, PersonDTO> typeMap = modelMapper.createTypeMap(Person.class, PersonDTO.class);
        typeMap.addMappings(mapper -> {
            mapper.skip(PersonDTO::setAge);
        });
    }

    @GetMapping("api/benchmark/qa")
    public PersonDTO qa() {
        Person p = new Person();
        p.setName("wangao");
        p.setAge(30);
        return modelMapper.map(p, PersonDTO.class);
    }
複製程式碼

發現非堆記憶體穩定,類載入數量穩定:

image-20190214150539240

image-20190214150752900

相關文章