1. 程式人生 > >一次JVM OOM問題的解決

一次JVM OOM問題的解決

單元 占用 找到 memory 日誌 map xls web rowset

背景:
我們的一個報表工具系統,核心功能當然是查看和下載,其中下載文件功能需要將報表數據都寫入文件中。一直以來,系統總是會因為JVM內存溢出而宕機。

現象:
從 weblogic 日誌裏看,宕機前拋出了大量java.lang.OutOfMemoryError: getNewTla錯誤信息,而且堆棧信息中能出現各種情況,而且有的很抽象,難以看出具體由某一個功能某一個方法導致的。後來想想,內存撐滿後,確實可能導致其他功能崩壞。

由於之前對JVM及檢測調試手段都不熟悉,因此通過一定時間的看書學習,期間也一直做些嘗試

第一次嘗試:
閱讀了下載功能的代碼後發現,邏輯中先進行了一次全量 sql 查詢返回一個 List,然後把List遍歷寫入文件。這個明顯不對,應該利用ResultSet批次查詢的特性邊查詢就邊寫入文件。不然下載過程List 持有的大量數據對象會占用很多內存。

但是改造後,效果只有輕微的提升,通過在JVM啟動參數添加 -XX:+PrintGC,查看下載過程的垃圾回收情況。內存開銷依然水漲船高,每次 gc 的效率都很低,很快就吃了幾百M。

第二次嘗試:
關註點放在了寫入的文件上,文件是 xls 格式的 excel 文件,經過一定了解,我得出這麽一個結論:excel 文件不是文本文件,無法直接將數據寫入文件尾部,無論是 jxl 還是 poi 這種 excel工具庫,一定是將全量數據以某種數據結構存於內存中,最後一次性寫入。因此這部分理論上是無法優化的。

其實到這裏,有點懵了

第三次嘗試:
這時候通過閱讀代碼已經很難猜測到還有什麽地方可以優化了。於是想著將下載過程中的內存快照給 dump 出來,然後通過工具看能否分析出什麽。

我的做法是:先通過 IDE 在邏輯結束前設置斷點,然後在命令行通過 jmap 命令,生成當前內存快照的 dump(hprof) 文件。最後通過分析工具 MAT 打開 dump 文件。

工具顯示,占用內存最大的兩部分,一部分和excel 工具 jxl 的某個類有關,占了70,還有一個和 sql 查詢某個類有關,占了30%。

a. SqlRowSet
這個對象持有了30%的開銷,於是在代碼中搜到了這行 SqlRowSet rs = this.getJdbcTemplate().queryForRowSet(sql);
了解後知道,這個類是對 ResultSet 的一種擴展,用法和 ResultSet 極為相似,區別在於,SqlRowSet會持有全量的查詢數據,那麽問題就在這裏了,這裏居然也有一份全量數據的引用。這就很尷尬了,由於代碼裏變量名都是用的 rs,而且用法一樣,導致之前一直以為用的是 ResultSet..... 修復完,再進行dump分析,這部分的開銷確實消滅了

b. WritableCellFormat
這個類持有了大部分的內存開銷,依然再代碼中找到了使用的地方。原來這個類是作為 excel 單元格對象附加的一個 Format對象。而代碼中對每一個單元格,都去 new 了一個 format 對象。通過 MAT 工具能查看每個對象實例的大小,一看200B,數量一多內存直接上去。這裏應該改成:只需要每一列 new 一個 format 對象,同一列的單元格共用一個唄

這兩個問題改完以後,確實問題都解決了。回過頭來看,如果一開始就比較熟練的話,完全可以將啟動參數 Xmx調小,同時加上 -XX:+HeapDumpOnOutOfMemoryError參數使得再 OOM 後自動生成 dump 文件,再用分析工具查看對象占用情況,快速解決問題。不過現在也是一個學習的過程,挺好。

一次JVM OOM問題的解決