【Java線程與內存分析工具】VisualVM與MAT簡明教程


前言

本文將簡要介紹Java線程與內存分析工具VisualVM和MAT的使用,進一步的學習可參考官網或工具幫助(例如MAT:Help -> Welcome -> Tutorials),並在實際工作中融會貫通。


VisualVM

Java VisualVM是JDK1.6后自帶的可視化工具,提供圖形界面以實時監控應用程序的線程狀態、CPU和內存資源消耗情況,並且可以保存快照以便脫機分析程序的性能瓶頸。

安裝與配置

JDK1.6之后已自帶VisualVM工具(jvisualvm.exe)。若使用非Oracle JDK,可自行登錄官網下載VisualVM並安裝。
工具下載后,需要在visualvm_143\etc\visualvm.conf里手工配置JDK路徑(visualvm_jdkhome)。

VisualVM可監控本地或遠程的Java程序。使用遠程監控時需要在服務端啟動JMX服務。首先,在遠程程序的啟動參數中增加如下JVM參數
-Djava.rmi.server.hostname=10.186.189.98(遠程服務器IP地址) -Dcom.sun.management.jmxremote.port=8090(JMX遠程監聽端口) -Dcom.sun.management.jmxremote.ssl=false(禁用SSL) -Dcom.sun.management.jmxremote.authenticate=false(不啟用用戶認證)

然后重啟遠程程序。此時,通過netstat -ano | findstr 8090(Windows)或netstat -anlp | grep 8090(Linux)查看端口已處於Listening狀態,表明可以進行遠程JMX連接。

除單獨使用VisualVM工具外,也可在IDEA中集成VisualVM launcher插件。通過File-> Setting-> Plugins -> Browers Repositrories搜索VisualVM Launcher安裝並重啟IDEA后,會出現菜單和按鈕兩種啟動方式:

點擊按鈕后會出現選擇VisualVM路徑,選擇VisualVM可執行文件即可。此后,點擊啟動會打開一個VisualVM窗口。

本地使用

本節結合代碼示例介紹VisualVM的界面功能。示例代碼如下:

package thread;

public class InfiniteLoop {
    public static void main(String[] args) {
        Thread t1 = new Thread(new ImplicitLoop(), "ImplicitLoop");
        Thread t2 = new Thread(new ExplicitLoop(),"ExplicitLoop");
        t1.start();
        t2.start();
    }
}

class ExplicitLoop extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("I work hard!");
        }
    }
}

class ImplicitLoop extends Thread {
    @Override
    public void run() {
        for (byte i = 0; i < 150; i += 2) {  //此處因數值溢出導致死循環
            System.out.println("I've worked " + i + " hours!");
            if (i >= 120) {
                try {
                    System.out.println("I'll take a short break...");
                    Thread.sleep(20);
                    System.out.println("I wake up!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

啟動VisualVM查看本地監控信息,界面如下:

左側"本地(Local)"下列出包含InfiniteLoop示例在內的本地Java進程,右側以概述(Overview)、監視(Monitor)、線程(Threads)、抽樣器(Sampler)等頁簽展示出詳細信息。

其中,概述頁可查看進程的基本信息、JVM啟動參數、系統屬性(同jinfo -sysprops <pid>)等信息。

監視頁可查看CPU、內存(堆與元空間)、類和線程的的實時折線圖。執行垃圾回收(Perform GC)按鈕可以觸發系統GC,堆Dump(Heap Dump)按鈕可在指定目錄生成堆轉儲(Dump)文件。

注意,本地監控時點擊堆Dump(Heap Dump)按鈕會自動加載打開生成的dump文件,而遠程監控時需要將遠程主機上生成的dump文件拷貝至本地再手工加載。此外,VisualVM加載分析內存Dump文件時非常緩慢,建議使用MAT來分析內存Dump

線程頁可詳細查看每個線程的運行時間及狀態。線程Dump(Thread Dump)按鈕可生成線程dump文件(類jstack <pid>)。

圖中,時間線里展示活動線程的運行、休眠(sleep)、等待(o.wait)、駐留(空閑)和監視(同步阻塞)狀態,並可通過縮放按鈕更細致地觀察線程狀態。
Threads inspector插件可展示單個或多個線程的堆棧。圖中僅勾選了ImplicitLoop線程,由堆棧可知其阻塞在System.out.println("I've worked " + i + " hours!")行——執行該方法會先加鎖!通過Refresh按鈕刷新堆棧,會發現ImplicitLoop線程有時會處於休眠狀態。

抽樣器頁以一定的時間間隔對CPU、內存進行采樣,可檢查出占用CPU時間較多或占用內存空間較大的線程,有助於性能調優。對CPU采樣時,該頁提供CPU樣例(CPU samples)和線程CPU時間(Thread CPU time)兩個子頁簽,前者可用於分析調用鏈上的方法耗時,后者可用於比較線程CPU耗時。

VisualVM還提供不少有用的插件,例如Visual GC(查看垃圾回收的狀態)。可通過工具(Tools) -> 插件(Plugins)下載插件。

遠程監控

在VisualVM左側點擊遠程(Remote) -> 添加遠程主機(Add Remote Host),填寫服務器IP地址。

然后點擊遠程主機,右鍵"添加JMX連接(Add JMX Connection)",填寫JMX端口號並勾選"不要求SSL連接(Do not require SSL)"。

在添加的JMX連接上右鍵"打開(Open)"或直接雙擊,在界面右側可看到監控面板。

MAT

MAT(Memory Analyzer Tool)是一個快速、功能豐富的JAVA堆轉儲文件分析工具,可幫助開發者發現內存泄漏和減少內存消耗。

使用場景

MAT常見的使用場景如下:

  • OOM(OutOfMemoryError異常),原因通常有:
    • 對象已死但無法通過垃圾收集器自動回收,內存不斷泄露——需找到泄露的代碼位置並加以修復
    • 產生大量生命周期太長或持有狀態時間過長的對象——除增大堆分配空間外,考慮優化存儲結構或代碼邏輯
  • CPU負載沖高、線程死鎖等(類似VisualVM)
  • 窺探內存對象的內容,例如:
    • 排障時環境不允許進行Debug調試或添加日志打印
    • 掃描內存中是否存在常駐的明文口令等敏感信息

安裝與配置

官網下載單機版MAT工具,解壓后直接運行MAT目錄的MemoryAnalyzer.exe即可啟動MAT。

若待分析的dump文件過大,可增大安裝目錄下MemoryAnalyzer.ini文件里的Xmx參數值(默認1G)。注意,Xmx取值不能大於運行環境的的系統內存,否則MAT啟動時會報錯Failed to create the Java Virtual Machine

獲得堆轉儲文件

MAT是一個靜態堆分析工具,需要預先抓取Java堆轉儲文件(內存快照)。

可通過以下幾種方式生成堆轉儲文件:

  1. 在JVM啟動參數里增加-XX:+HeapDumpOnOutOfMemoryError參數,系統發生OOM時會自動在工作目錄(user.dir)生成java_pid<pid>.hprof轉儲文件。還可通過JVM參數-XX:HeapDumpPath=<path>顯式指定堆轉儲文件的存放路徑。
  2. 如果不想等到發生OOM錯誤時才獲得堆轉儲文件,可添加JVM參數-XX:+HeapDumpOnCtrlBreak,以便在控制台使用Ctrl+Break(Pause)鍵來按需獲取堆轉儲文件。
  3. 若環境上Jmap工具可用,則可通過jmap -dump:live,format=b,file=heap.bin <pid>命令獲得轉儲文件。
    其中,pid為進程ID,live選項會在轉儲前強制觸發一次full GC(以減小文件體積),file可指定產生文件的目錄和名稱。
    類似地,VisualVM、Jconsole等JDK工具也可用來生成堆轉儲文件。
  4. MAT本身也可獲取堆轉儲文件,即File -> Acquire Heap Dump菜單。

分析堆轉儲文件

本節亦結合代碼示例介紹MAT常見的界面功能。示例代碼如下:

package thread;

import java.util.*;

public class JavaHeapDump {
    private static List<String> smallArray = new ArrayList<>();
    private static List<byte[]> largeArray = new ArrayList<>();

    public static String getPassword() {
        char[] pw = {'A', 'd', 'm', 'i', 'n', '1', '2', '3'};
        return new String(pw);
    }

    public static void makeHeapOom() {
        for (int i = 0; i < 1000; i++) {
            smallArray.add(getPassword()); //smallArray.add(getPassword().intern());

            byte[] elems = new byte[1024 * 1024];
            Arrays.fill(elems, (byte)101);
            largeArray.add(elems);
            //largeArray.add(new byte[1024 * 1024]);
        }
    }

    public static void main(String[] args) {
        makeHeapOom();
    }
}

編譯代碼后以-Xms20m -Xmx20m(限制堆空間以盡快OOM)等JVM參數運行,得到如下輸出:

D:\xywang\target\classes>java -Xms20m -Xmx20m -Xmn2m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\dump\heapDump.bin thread.JavaHeapDum
p
java.lang.OutOfMemoryError: Java heap space
Dumping heap to E:\dump\heapDump.bin ...
Heap dump file created [21458899 bytes in 0.805 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at thread.JavaHeapDump.makeHeapOom(JavaHeapDump.java:18)
        at thread.JavaHeapDump.main(JavaHeapDump.java:26)

可知,很快就出現內存溢出(java.lang.OutOfMemoryError: Java heap space),並在E盤下生成heapDump.bin轉儲文件。

此時啟動MAT,選擇菜單項File -> Open Heap Dump來加載待分析的堆轉儲文件。加載完文件后,在彈出的向導頁面選擇按照內存泄漏模式分析。

Leak Suspect Report是默認生成的可能存在潛在內存泄露的分析報告,在餅圖中描述了各種問題占用內存的比例,餅圖下方則是關於潛在問題的細節分析。


點擊"Details"鏈接,可看到引起內存溢出可能的最大元凶確實為largeArray!此處,"Shortest Paths To the Accumulation Point"展示由於和哪個GC Root相連導致當前Retained Heap占用相當大的對象無法被回收。

概覽頁簽提供了Heap Dump的概覽,包括堆的餅圖以及Actions/Reports/Step by Step等快速訪問功能區。

其中,Histogram(堆直方圖)提供按類分組的對象的內存占用統計列表,默認按照某個類的shallow heap從大到小排序。Dominator Tree(支配樹)顯示按照Object/Class保留內存大小排序的結果,可用於排查哪些對象導致其他對象無法被垃圾收集器回收。Top Consumers是Dominator Tree數據的圖形統計,分別按照Object、Class,ClassLoader,Package等維度做的內存占用統計。Top Components列出占用堆空間較多的組件,並給出可以減少內存消耗的建議。

以最常用的Dominator Tree界面為例:

堆中有兩個ArrayList,且其中一個占用了96.57%的內存。以下簡要介紹圖中主要字段的含義:

Shallow Heap:對象自身所占用的內存大小,不含其引用的對象所占的內存大小。數組對象的Shallow Heap是數組元素大小的總和,非數組對象的Shallow Heap是對象所有成員變量大小的總和。
Retained Heap:當前對象大小 + 當前對象可直接或間接引用到的對象的大小總和,即當前對象被GC后從Heap上總共能釋放掉的內存。
incoming references:當前類被哪些類引用,或當前對象被哪些對象引用。
outgoing references:當前類的所有實例,或當前對象所引用的對象。

選中Dominator Tree中占用內存最大的對象,通過with incoming references查看持有其引用的外部對象。

可見,占用大量內容的元凶正是largeArray。對於集合對象,可右鍵選擇Java Collections的子菜單做各種排序和查看。例如,圖中選擇Extract List Values查看largeArray的內容,結果如下所示:

窺探對象內存值

JavaHeapDump示例代碼中有意使用到密碼,真實業務中可通過OQL(Object Query Language)排查內存中是否存在此類敏感信息。

OQL是一種基於javascript表達式的語言,它將類當作表、該類的實例對象當作記錄行、對象中的成員變量當作表中的字段,可以用類似SQL語句的方式查詢Java堆中的對象。OQL語法結構如下:

select <JavaScript expression to select>
from [ instanceof ] <class name="name">
[ where <JavaScript boolean expression to filter> ]

更多OOL的語法,請在OOL頁面上按F1鍵查看幫助信息。

在MAT工具欄中點擊OQL按鈕,打開OQL編輯器窗口,輸入查詢命令后點擊紅色感嘆號按鈕進行查詢,結果如下:

注意,查詢語句中"Admin123"后面的".*"相當於SQL通配符"%"。查詢結果中赫然可見"Admin123"這樣的明文密碼!

通過Merge Shortest Path to GC Roots查看這些密碼對象到GC Roots是否可達:

若該對象為unreachable則說明密碼不是常駐內存,可見圖中的密碼均常駐內存。

堆轉儲文件對比分析

實際業務場景中堆中內存對象可能非常多,定位內存泄露時,通常需要抓取和對比先后兩個時刻的堆轉儲文件。MAT操作步驟如下:

  1. 加載第一個堆轉儲文件,並打開Histogram視圖。
  2. 打開Window -> Navigation History視圖,在histogram右鍵選擇Add to Compare Basket。
  3. 加載第二個堆轉儲文件,也添加到Compare Basket中。
  4. 打開Window -> Compare Basket視圖,點擊Compare the Results(右上角的紅色嘆號)。
  5. 在Compared Tables里分析對比結果。

    例如,圖中#1是使用String.intern()存儲密碼后的內存信息,比#0創建的String對象要少(這由OQL結果也可證明)。

通過這種方式可快速定位到操作前后所持有的對象增量,從而進一步定位出導致內存泄露的具體元凶。

總結

本文簡要介紹了Java線程與內存分析工具VisualVM和MAT的使用,進一步的學習可參考官網或工具幫助(例如MAT:Help -> Welcome -> Tutorials),並在實際工作中融會貫通。真是無話可說了……


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM