AIX 平臺上基於 IBM JDK 的 Java 應用內存泄漏分析
在 IBM Bluemix 雲平臺上開發並部署您的下一個應用。
引言
Java 開發者一般不需要考慮內存釋放問題,全交由 GC 去處理。但是在一些生產環境中,JVM 經過長時間運行後,即使是一些很小的未釋放的 Java 對象,日積月累也會導致內存資源枯竭,最終使 Java 應用崩潰的問題。本文將就一個 AIX 平臺上基於 IBM JDK 開發的 Java 應用內存枯竭的實際案例分析過程,來引領讀者理解基於 IBM JDK 的 Java 應用內存泄漏調查方法,以及分析思路。
第一步,判斷是否是內存泄漏問題
根據生產環境出現的錯誤日誌以及 GC 日誌文件,進行初步判斷是否是內存泄露問題。
Java 應用的錯誤日誌:
“***WARNING*** Java heap is almost exhausted: 4% free Java heap
應用程序中對可用內存做了判斷,當可用內存比較低的時候輸出了 WARNING 的日誌。
使用 IBM pattern modeling and Analysis Tools for Java Garbage Collector 來分析 GC 日誌。
圖 1. 選擇打開 IBM JDK 的 GC 日誌文件
圖 2. 點擊 Graph View Part 顯示
圖 3. 顯示 GC 分析圖
從圖中可以看出 Java 內存的堆 (Heap) 的使用情況是持續的上升趨勢。
由此我們可以得出結論,Java 應用程序存在內存泄漏問題,導致內存堆得不到釋放。
第二步,截取 Java 內存堆的轉存儲文件
在得出是內存堆泄漏的問題結論後,接下來就需要取得內存堆的轉存儲文件來做進一步分析。
在 AIX 平臺上截取 IBM JDK 的內存堆的轉存儲文件前,需要先對 IBM JDK 的 JVM 參數進行設置。有 2 種設置方式:
- 設置 IBM JDK 的全局變量:
export IBM_HEAPDUMP=true
- 添加 JVM 啓動參數:
-Xdump:system+heap+java:events=user,request=exclusive+prepwalk+compact
設定完後需要重啓 JVM, 使設定生效。然後可以在 kill -QUIT pid 命令來生成轉存儲文件 (Dump),pid 爲實際啓動的 JVM 進程 ID。
當內存泄漏情況非常小且緩慢的時候,無法從 1 個或 2 個轉存儲文件中分析出導致泄漏的 Java 對象。根據上面 GC 的日誌趨勢,制定如下的轉存儲文件的截取的方案。
- 截取週期爲 1 星期以上,每天一次。
- 每天固定時間截取,且避開發生大的 GC 的時間段。
這樣可以得到幾個可以用來比對分析的轉存儲文件,以及避免正在運行中得一些 Java 對象對於分析的干擾。
第三步,分析轉存儲文件
使用 MAT (Memory Analyzer Tool) 工具來分析轉存儲文件。由於實際轉存儲文件非常大,需要調整 MAT 工具的啓動參數文件(MemoryAnalyzer.ini),32 位的 window 平臺的話,最大也只能設定到 1.5G。因此當分析超大的轉存儲文件時,建議在 64 位 window 平臺上做,這樣可以分配更多的內存給 MAT 工具使用。
1)查找可疑泄漏點
在 MAT 的 Overview 中,可以點擊”Leak Suspect”來生成 Leak Suspect Reports, 做最直觀的分析。
圖 4. 點擊 Leak Suspect
圖 5. 顯示某 1 天的轉存儲文件分析結果。
如果連續幾天的轉存儲文件中,都是這個 Suspect 實例 (Instance) 的所佔比例最大,且所佔內存空間也在不斷上升,沒有下降的趨勢的話,那基本上可以斷定該實例是發生泄漏的對象了。
點擊打開該 Suspect 的 Detail 信息。
圖 6. 點擊 Details 鏈接
通過比對連續幾天的轉存儲文件,可以發現是 Hashtable 中得 Entry 對象的佔用空間不斷變大。
圖 7. 顯示 Detail 信息
那接下來進一步深入分析,到底在 Hashtable 中佔用空間增大到底是什麼實例。
2)深入分析
點擊 Suspect 實例,打開該實例的 Dominator Tree。
圖 8. 選擇 Dominator Tree 選項
可以在 Dominator Tree 中看到 Hashtable 中放的 Java Instance,依次爲
Company[] -> Event[] -> Task (Manager, Handler, xxxxx)
圖 9. 顯示 Dominator Tree 信息
分析其中 1 個複雜的 Task,點擊 Path to GC Roots 繼續深入分析 Task 的引用關係。Weak 和 Soft 引用會在 Major GC 是被釋放,所以查看下不包含他們的引用關係。
圖 10. 顯示可疑點的引用關係圖
根據 Java 應用的代碼調查,Company 和 Event 是常駐於 Service 靜態實例中。
引用 A 代碼分析
引用 A 的順序 Task <- Thread <- Record.Hashtable。Record 中得 Hashtable 中有對一個 Thread 的引用是比較奇怪的。因爲那將導致這個 Thread 的實例沒法釋放,從而導致 Task 的實例沒釋放。查看 Java 應用代碼發現,Thread 的實例被放入 Record 實例的靜態 Hashtable 中,但是沒有調用 Remove。
清單 1
public class XXXXXX extends XXXXXBase { // … private static Hashtable currentXXXXXXX = new Hashtable(); // … public void process (xxxx){ // … currentXXXXXX.put(Thread.currentThread(), XXXX_); // … }
引用 B 代碼分析
和引用 A 相似,Thread 被放入了 Factory 的靜態實例的 Hashtable 中,而且沒有 Remove。
引用 C 代碼分析
Task 是經由 Event 每次新建實例來啓動執行,當執行完後應當銷燬該 Task 的實例,不應長期存在於內存中。上圖的應用分析顯示 Event 中引用了 Task 的實例,因此 Task 沒法釋放。查看 Event 的代碼證明了確認如此,沒有將新建的 Task 實例重設爲 Null。
圖 11 引用分析結構圖
直接用 OQL(Object Query Language) 來查詢該 Task 實例,可以看到該 Task 的實例隨着時間不但增多。
圖 12. OQL 查詢結果
綜上所述,由於強引用的關係存在於靜態實例中,所以 Task 的實例沒法釋放,最終導致了內存枯竭。Java 內存堆泄漏的問題,多發生在靜態 Hashtable、Hashmap、Vector 的使用不當,還有諸如打開文件後沒有關閉,DB 和 Socket 連接打開沒有關閉之類的都會導致 GC 無法釋放引用的 Java 實例。
本文中所描述的通過 Java 內存堆和 GC 日誌來分析內存泄漏方法,以及 Eclipse MAT 和 IBM Pattern Modeling and Analysis Tool for Java Garbage Collector 工具適用於調查任何平臺上的 Java 應用程序。但文中提及的截取 Java 內存堆的轉存儲文件方法只限於在 AIX 平臺上的 IBM JDK。針對 Linux, Window 等平臺,或 Sun JDK 等有專門的截取方法,不在本文中一一描述。
結束語
本文通過對一個實際內存泄漏的分析,以及一些實際使用中的工具和經驗技巧的介紹,展示裏分析 Java 內存分析的常規方法。