使用jmap和MAT觀察Java程序內存數據

RoseannaFor 10年前發布 | 26K 次閱讀 Java開發

來自: http://www.xinitek.com/blog/2014-06-23_%E4%BD%BF%E7%94%A8jmap%E5%92%8CMAT%E8%A7%82%E5%AF%9FJava%E...


使用jmap和MAT觀察Java程序內存數據

背景

很多故障跟數據結構中實際存儲的數值會很有關系。有時候我們能夠預感到這類數據,例如:

  1. 某些 成員變量緩存池當前尺寸(size)、容積上限(capacity)
    為了加快服務響應速度,通常會把一些相對靜態的內容在內存中做緩存。內存中容納這些內容的容器稱為緩存池緩存池已存對象的數目,即緩存池當前尺寸。這個當前尺寸與該緩存池實際占用內存量息息相關。
    由于內存有限,緩存池通常會設置容積上限。這個值可以控制緩存池 占用內存量的最大值。

  2. 某些類的對象數量、某個對象導致的無法回收內存量retained size

  3. 局部變量的值

有經驗的開發人員可能將這些數值打印到日志中,便于故障分析。

但很多時候我們并沒有預先打印這些數值,或者很計算出這些數值(如無法回收內存量)。在沒有日志記錄的情況下,該如何獲得這些數值?

答案是使用jmapMAT(Memory Analyzer Tool)!

工具介紹

  • jmap是JDK自帶的命令行工具,用它可以將指定進程的整個Java堆(Java Heap)轉儲到指定的數據文件
  • MAT是一個第三方工具,可以從http://www.eclipse.org/mat/下載。必須在圖形界面(GUI)中執行(例如Windows或Linux的VNC桌面系統中)。用它可以對數據文件做非常詳盡的分析。

它們之間的關系圖示如下:

工具使用

jmap使用方法:

jmap -dump:file=dump.map <pid> 

執行后,會生成dump.map文件。這個文件可以用MAT打開。
如果你在遠程Linux系統中工作,可能需要使用VNC(具體方法參考: (TODO) )。

啟動MAT的方法(在Linux桌面中,打開一個終端(Terminal),然后執行):

#假設mat工具已經解壓到/usr/local/mat中 cd /usr/local/mat #如果dump.map文件尺寸較大(例如2GB以上),需要修改MemoryAnalyzer.ini文件中 #-Xmx選項。將它設置到一個更大的值,例如4GB左右: -Xmx4000m #啟動Memory Analyzer ./MemoryAnalyzer 

啟動后,在菜單中選擇Open Heap Dump,讀入dump.map文件即可開始分析。

工具說明

jmap生成的數據文件(例如dump.map)中有詳盡的Java堆(Java Heap)的信息,這些信息包括—所有的Java對象的數據成員,以及它們之間的引用關系。并且MAT又是一個非常棒的工具,因此上述內容都可以看到。并且MAT還為內存泄漏的分析有特別的支持,它能夠計算出每個對象的占用內存量(Retained Size)。將對象按照占用內存量(Retained Size)從大到小排序,通常很容易就能確定究竟是什么對象發生了內存泄漏

在MAT中打開數據文件(例如dump.map)后,通常可以通過如下幾個方式來查看:

  1. Leak Suspect: Memory Analyzer智能分析出的最可能發生內存泄漏的對象,通常也就是占用內存量(Retained Size)最大的對象
  2. Dominator Tree查看,常用的兩種方式:
    • 按照占用內存量(Retained Size)從大到小排序(Dominator Tree的默認查看方式)
    • 正則表達式匹配類名,查看對應的類(Class)的對象(Object),或者對應類的靜態成員(Static Member)
  3. Threads View查看:可以看到各個線程的調用棧,以及局部引用的值
  4. Object Query Language查詢:可以用類似SQL的語句查詢整個Java Heap堆

使用MAT不但可以觀察(Inspect)所有對象的成員變量的值,如果變量是引用,還可以跟蹤(follow)引用(reference)進一步查看相關對象的數據。

示例

撰寫程序如下(MatExample.java):

import java.util.List; import java.util.LinkedList; public class MatExample {     static String staticVar = "Hello, I am static";     private String instanceVar = "Hello, I am instance";     public static void main(String[] args) throws Exception     {         MatExample instance = new MatExample();         List<String> leakList = new LinkedList<String>();         int k = 0;         for(int i=0; i<10000; i++)         {             k++;             leakList.add(k+"");         }         System.out.println("10000 objects generated");         while(true)         {             Thread.sleep(1000);         }     } } 

編譯(生成MatExample.class)

javac MatExample.java 

運行(MatExample.class)

java MatExample 

程序輸出:

1000 objects generated 

另開一個命令行,然后使用jps列出所有Java進程:

jps -mlvV 

jps輸出類似如下信息:

91888 sun.tools.jps.Jps -mlvV -Dapplication.home=E:\Java\jdk1.7.0_45 -Xms8m 90752 MatExample 

上面90752便是MatExample的進程編號(PID),現在我們運行jmap獲取數據文件(存儲到dump.map):

jmap -dump:file=dump.map 90752 

接著啟動MAT,并通過菜單中 File->Open Heap Dump 打開dump.map,如圖:

MAT會讀取數據文件到內存中并作必要的預處理(如果是較大的文件,則耗時較長,建議去喝杯茶休息下)。

MAT讀取完文件,會出現一個向導,讓我們選擇一種分析方式。我們可以選擇泄漏嫌疑對象報告(Leak Suspects Report)分析,然后點“Finish”。

MAT呈現出一個餅圖描述該進程內存的分布情況,在此圖形中可以清晰地看到內存占用最大的對象。下方,還會有Top N的泄露嫌疑對象文字描述,如圖:

這里說的是局部變量占用“721,136 (63.90%) bytes”,并且是一個LinkedList對象造成的。點擊“See stacktrace”,就能看到調用棧

main   at java.lang.Thread.sleep(J)V (Native Method)   at MatExample.main([Ljava/lang/String;)V (MatExample.java:21) 

線程名稱是“main”。點擊圖示的按鈕,可以打開線程視圖(Threads view),查看帶有局部變量的調用棧信息。

從中可以清晰看到是在MatExample.main函數中引用的一個LinkedList占用了720,040字節。

我們可以點擊菜單中Window -> Inspector,然后查看該LinkedList對象的屬性。

可見,其size屬性確實是10000。可見泄露嫌疑對象報告(Leak Suspect Report)能夠幫助我們較迅速地定位到內存泄漏點。

另外,也可以從內存占用最大的對象的思路來找內存問題,點擊圖示圖標,打開Dominator Tree View

將最上面的一行逐行展開,也可以看到LinkedList的內存占用較大。

下面嘗試查看指定類型(class)的靜態成員(static member)和對象

在Dominator Tree的正則輸入框(Regex)中輸入“MatExample”:

回車后,顯示下圖:

“class MatExample”的屬性和引用,與代碼中定義的MatExample類的靜態成員(static member)的內容相符合:

static String staticVar = "Hello, I am static"; 

我們可以嘗試下跟蹤引用:在Inspector窗口中,對一個引用點擊右鍵,然后選擇“Go Into”

即可看到該對象的屬性。

如果希望讓引用對象顯示在右圖中,可以選擇"List objects -> with outgoing references”

這樣可以更方便地進一步跟蹤其引用。

但我們發現,代碼中創建的MatExample對象

MatExample instance = new MatExample(); 

并沒有出現在之前的圖中

可以嘗試下Object Query Language (OQL)。點擊圖標

輸入查詢語句

select * from INSTANCEOF MatExample 

點擊執行按鈕

現在我們可以查到所有MatExample對象(本例只有一個):

點選后,在Inspector窗口中,同樣可以看到其成員變量的值。這里就不詳述了。

總結

jmap和MAT是一對非常強大的工具,通過它們可以獲知一個Java進程中所有Java級別(與Native級別相對)的數據內容。包括所有類、對象的值、引用關系、線程調用棧、局部引用,以及每個對象占用內存量等信息。而這些信息對故障追蹤可能會起到非常大的幫助。


 本文由用戶 RoseannaFor 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!