java 堆內存泄露排查(例子)


配置說明

  • 系統:Windows10
  • 項目:KeyboardPiano V1.7
  • 對象:音頻播放類 com.sun.media.sound.DirectAudioDevice$DirectClip
  • 原因:sun 的老舊框架,Clip.close(),音頻數據 audioData[] 無法釋放,從而導致堆內存泄露
  • 工具:JConsole、Memory Analyzer、Eclipse

項目簡介,KeyboardPiano 是基於 java 實現的鍵盤鋼琴,其原理是按一個鍵播放一段音頻


排查之路

視頻教程

注意:請讀者務必安裝 Memory Analyzer,才能進行相應操作

圖文教程

  • 運行項目,經過大量按鍵後,查看任務管理器,出現內存猛增的情況,且有增無減(img1

  • img1 任務管理器內存情況
    img1

由於資源管理器並不能顯示更多的內存消息,所以藉助 JConsole 查看內存的泄露類型(堆內img2/堆外img3

  • img2 堆內存
    img2

  • img3 非堆內存
    img3

有上圖可知,此處發生的是堆內存泄露,非堆內存處於可接受範圍內,爲了查看堆內存細節,這裏使用 Memory Analyzer 做內存分析

參照以上教程,排查內存泄露觸發點

整體流程:限制內存大小,製造溢出,查找溢出

  • 鼠標右鍵點擊主類 KeyboardPiano => Run As => Run Configurations

  • 設置以下參數

    1. -Xms50m JVM初始分配的堆內存,設置最小堆內存爲 50 M
    2. -Xmx50m JVM最大允許分配的堆內存,設置最大堆內存爲 50 M
    3. -XX:+HeapDumpOnOutOfMemoryError 當出現 OOM 時進行 HeapDump
    4. -XX:HeapDumpPath 設置 dump 文件輸出路徑 (請讀者務必修改成自己的路徑)
-Xms50m -Xmx50m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=E:\Java_code\piano_tmp\memory_test
  • img4 圖裏面參數僅作參考,以上面代碼爲準
    img4

  • 在一系列猛如虎的按鍵操作後,JVM 承受不住外界的壓力,果斷溢出報錯

  • img5 溢出報錯
    img6

  • 接着在 eclipse 打開 *.hprof 文件,選擇 Leak Suspects Report, 點擊 Finish

  • 分析查看可疑點,博主在 Problem Suspect 2 發現異常,如下圖

  • img6 Problem Suspect 2
    img6

  • 說明 DirectClip 有問題,切換到 Overview 窗口,點擊查看 Dominator Tree

  • 按照 Retained Heap 排序,可以看到一堆的 DirectClip

  • 隨便找一個展開,發現 byte[] 佔據了絕大多數的內存,列爲重點懷疑對象

  • img7 Dominator Tree
    img7

  • 接着鼠標右鍵點開其中一個 DirectClip 對象,選擇 List objects => with outgoing references 查看外部引用

  • 展開後發現,byte[] audioData 就是罪魁禍首,從名字可以看出,音頻的數據就保存在該 byte 數組

  • img8 DirectClip 內存分配情況
    img8

  • 最後,根據博主測試得出,DirectClip 即使關閉了,audioData 也得不到釋放,才導致了堆內存泄露。所以可以說,這是 sun 老舊框架的 BUG,並非自身程序的問題

  • 那泄露的關鍵點已經找到了,如何修正 BUG 呢?詳細見 KeyboardPianoV1.7.2 Debug(音頻優化) 關鍵在於更換播放方式


數據表格

  • JConsole 內存分析數據表(主要參考數據爲高亮部分),內存分類以及相關拓展資料請見 相關鏈接
Memories initialized increasing total
1. Heap Memory Usage 100 250 350
2. Non-Heap Memory Usage 22 10 32
3. Memory Pool "PS Old Gen" 0 220 220
4. Memory Pool "PS Eden Space" 100 0 100
5. Memory Pool "PS Survivor Space" 0 22 22
6. Memory Pool “Metaspace” 16 1 17
7. Memory Pool “Code Cache” 5 7 12
8. Memory Pool “Compressed Class Space” 2 0 2

相關鏈接

  • 內存管理機制 ← 以下說明摘抄於該教程

    通常,會認爲在堆上分配對象的代價比較大,但是GC卻優化了這一操作:
    C++中,在堆上分配一塊內存,會查找一塊適用的內存加以分配,如果對象銷燬,這塊內存就可以重用;
    而Java中,就想一條長的帶子,每分配一個新的對象,Java的“堆指針”就向後移動到尚未分配的區域

    但是這種工作方式有一個問題:如果頻繁的申請內存,資源將會耗盡。這時GC就介入了進來,它會回收空間,並使堆中的對象排列更緊湊。這樣,就始終會有足夠大的內存空間可以分配。

  • 內存分類 深入淺出的典例

    1. 伊甸園空間(堆):大多數對象最初分配內存的池
    2. 生存空間(堆):包含伊甸園空間垃圾收集後生存的對象。
    3. 年老代(堆):池包含已經存在一段時間的對象
    4. 永久代(非堆):池包含的所有虛擬機本身的反射的數據,如類和方法的對象。 Java虛擬機,使用類數據共享,這一代分爲只讀和讀寫區域。
    5. 代碼緩存(非堆):HotSpot Java虛擬機的還包括一個代碼緩存,包含內存,使用本機代碼的編譯和存儲。
  • 緩存機制詳解

    1. JAVA面試——緩存
    2. 用Java實現多種緩存機制
  • Linux 堆外內存的排查參考

    1. DirectByteBuffer堆外內存溢出問題排查
    2. 記一次JVM堆外內存泄露Bug的查找
    3. 使用google perf工具來排查堆外內存佔用

後記

內存泄露是純代碼層面的問題, 而內存泄露處理則是爲了提高程序的健壯性

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