一些JDK自帶的效能分析利器

CodeTiger發表於2022-03-20

有時候碰到伺服器CPU飆升或者程式卡死之類的問題,一般都不太好定位。這類bug一般都隱藏的比較深並且還可能是偶發性的,比較棘手。

對於此類問題,一般我們都有固定的分析流程。藉助於JDK自帶的一些分析工具,比如jstack、jmap、jstat一類的命令列工具,除此之外,還有jconsole、mat、jvisualvm這些圖形介面分析工具。

這篇文章基於JDK8,作業系統是macOS 12.0.1

1、一些命令列分析工具

這些命令列分析工具都在jdk/bin目錄下

image-20220316160402785-7417844.png

解壓jdk/lib/tool.jar可以得到上述工具的class檔案

image-20220316160755917-7418077.png

1.1 jps - JVM Process Status Tool

作用:列出正在執行的虛擬機器程式,並顯示虛擬機器執行主類名稱以及這些程式的本地虛擬機器唯一ID。

image-20220316193716333-7430637.png

第一個引數說明:

  • -q:預設攜帶的引數,顯示程式ID。

  • -m:顯示程式ID,主類名稱,以及傳入main方法的引數。

  • -l:顯示程式ID,主類全名。

  • -v:顯示程式ID,主類名稱,以及傳入JVM的引數。

  • -V:顯示程式ID,主類名稱。

[-mlvV]可以任意組合使用。

第二個引數說明:

  • hostid:伺服器的ip地址。不指定的情況下,預設為當前伺服器。如果要檢視其他機器上的JVM程式,需要在待檢視機器上啟動jstatd。

1.2 jstat - JVM Statistics Monitoring Tool

作用:監視虛擬機器各種執行狀態資訊,可以顯示本地或者遠端虛擬機器程式中的類裝載、記憶體、垃圾收集、JIT編譯等執行資料。

命令格式:jstat -

引數說明

第一個引數:option,代表使用者希望查詢的虛擬機器資訊,主要分為3類:類裝載、垃圾收集和執行期編譯情況。具體選項如下:

  • -class:顯示有關類載入器行為的統計資訊。

  • -compiler:顯示有關java hotspot vm即時編譯器行為的統計資訊。

  • -gc:顯示有關垃圾收集堆行為的統計資訊。

  • -gccapacity:顯示有關各個垃圾回收代容量及其相應容量的統計資訊。

  • -gccause:顯示有關垃圾收集統計資訊(同-gcutil),以及上一次和當前垃圾收集事件的原因。

  • -gcnew:顯示新生代行為的統計資訊。

  • -gcnewcapacity:顯示有關新生代大小及其相應空間的統計資訊。

  • -gcold:顯示老年代行為的統計資訊和元空間統計資訊。

  • -gcoldcapacity:顯示有關老年代大小的統計資訊。

  • -gcmetacapacity:顯示有關元空間大小的統計資訊。

  • -gcutil:顯示有關垃圾收集統計資訊。

  • -printcompilation:顯示java hotspot vm編譯方法統計資訊。

第二個引數:vmid。如果是本地虛擬機器程式,那麼vmid和本地虛擬機器唯一ID是一致的。如果是遠端虛擬機器程式,那麼vmid的格式是:protocol:lvmid[@hostname[:port]/servername]

第三個引數:interval。取樣間隔,單位為s或ms,預設單位是ms,必須為整數。指定該引數,jstat命令將在每個間隔產生輸出。

第四個引數:count。要顯示的樣本數。

import java.io.IOException;

/**
 * -class:
 *  Loaded:已載入的類數量。
 *  Bytes:已載入的kb數。
 *  Unloaded:解除安裝的類數量。
 *  Bytes:解除安裝的kb數。
 *  Time:執行類載入和解除安裝的耗時。
 *
 * -compiler:
 *  Compiled:執行的編譯任務數。
 *  Failed:編譯失敗的任務數。
 *  Invalid:無效的編譯任務數。
 *  Time:執行編譯任務所花費的時間。
 *  FailedType:失敗的編譯型別。
 *  FailedMethod:失敗的編譯類名和方法。
 */
public class Demo1_jstat {
    public static void main(String[] args) throws IOException {
        System.out.println("jstat");
        System.in.read();
    }
}

啟動上面的demo後

import java.io.IOException;

/**
 * -gc: 垃圾收集的堆統計資訊
 *  S0C:s0區的容量(kb)
 *  S1C:s1區的容量(kb)
 *  S0U:s0區的使用大小(kb)
 *  S1U:s1區的使用大學(kb)
 *  EC:eden區的容量(kb)
 *  EU:eden區的使用大小(kb)
 *  OC:老年代容量(kb)
 *  OU:老年代的使用大小(kb)
 *  MC:元空間的容量(kb)
 *  MU:元空間的使用大小(kb)
 *  CCSC:壓縮的類空間容量(kb)
 *  CCSU:使用的壓縮類空間大小(kb)
 *  YGC:新生代垃圾收集次數
 *  YGCT:新生代垃圾回收時間
 *  FGC:full gc收集次數
 *  FGCT:full gc垃圾回收時間
 *  GCT:總垃圾收集時間
 *
 * -gcutil:垃圾收集統計資訊
 *  S0:s0區的使用率
 *  S1:s1區的使用率
 *  E:eden區使用率
 *  O:老年代使用率
 *  M:元空間使用率
 *  CCS:壓縮類空間使用率
 *  YGC:新生代gc次數
 *  YGCT:新生代gc耗費時間
 *  FGC:full gc次數
 *  FGCT:full gc耗費時間
 *  GCT:總垃圾收集時間
 */
public class Demo2_jstat {
    // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
    public static void main(String[] args) throws IOException {
        final int _1MB = 1024 * 1024;
        // 申請2M的空間
        byte[] b1 = new byte[2 * _1MB];
        System.out.println("1111");
        System.in.read();

        // 申請2M的空間
        byte[] b2 = new byte[2 * _1MB];
        System.out.println("2222");
        System.in.read();

        // 申請2M的空間
        byte[] b3 = new byte[2 * _1MB];
        System.out.println("3333");
        System.in.read();
    }
}

啟動上面的程式之前,先指定一些jvm引數

-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc

image-20220316211331049-7436412.png

然後啟動demo

image-20220316211628805-7436590.png

剛啟動時,程式會申請2M的陣列,可以看到eden區的使用率是62.37%,ygc的值為0。

讓程式申請第二個2M的陣列,之後再檢視堆記憶體資訊

image-20220316211756657-7436677.png

可以發現eden區的使用率達到了87.37%。

讓程式申請第三個2M的陣列,發現控制檯輸出提示

image-20220316211928010-7436769.png

再檢視堆記憶體資訊

image-20220316211957942-7436799.png

發現eden區的使用率降到了26.71,s1區使用率時70.80%,老年代的使用率是40%即4096kb也就是4M空間大小。同時ygc的次數是1,說明進行了一次ygc,並且把大物件放進了老年代中。

1.3 jinfo - Configuration Info For Java

作用:實時的檢視和調整虛擬機器各項引數。

命令格式:jinfo [option]

引數說明

第一個引數:option。

  • no options:輸出全部的引數和系統屬性。

  • -flag name:輸出對應名稱的引數。

  • -flag [+ | -] name:開啟或者關閉對應名稱的引數。

  • -flag name=value:設定對於名稱引數的引數值。

  • -flags:輸出全部的引數。

  • -sysprops:輸出全部的系統屬性。

命令演示

  • jinfo pid

  • jinfo -flag PrintGCDetails pid (輸出PrintGCDetails引數的值)

  • jinfo -flag +PrintGCDetails pid (開啟PrintGCDetails引數)

  • jinfo -flag HeapDumpPath=/ pid (設定HeapDumpPath引數的值)

在我電腦上使用這個命令會報錯,據說是macOS的一個bug,需要升級到jdk9,懶得升了。bug連結

image-20220316233053701-7444655.png

1.4 jmap - Memory Map For Java

作用:一個多功能的命令,它可以生成Java程式的dump檔案,也可以檢視堆內物件的資訊、classloader的資訊和finalizer佇列。

命令格式:jmap [option]

引數解釋

第一個引數:option

  • no option:檢視程式的記憶體映象資訊。

  • -heap:顯示Java堆詳細資訊。

  • -histo[:live]:顯示堆中物件的統計資訊。

  • -clstats:列印類載入器資訊。

  • -finalizerinfo:顯示在F-Queue佇列等待Finalizer執行緒執行finaizer方法的物件。

  • -dump::生成堆轉儲快照。

命令演示

  • jamp pid。列印出虛擬機器載入的每個共享物件的起始地址、對映大小以及共享物件檔案的路徑全稱。

  • jamp -heap pid。列印一個堆的摘要資訊,包括使用的GC演算法、堆配置資訊和各記憶體區域記憶體使用資訊。

  • jmap -histo:live pid。顯示堆中物件的統計資訊,包括每個Java型別,物件數量,記憶體大小(單位位元組),完全限定的類名。列印的虛擬機器內部的類名稱將會帶一個‘*’字首。如果指定了live子選項,則只計算活動的物件。

  • jmap -clstats pid。列印Java堆記憶體的永久儲存區域的類載入器的智慧統計資訊。對於每個類載入器而言,它的名稱、活躍度、地址、父類載入器、它所載入的類的數量和大小都會列印。

  • jmap -finalizerinfo pid。列印等待終結的物件資訊。Number of objects pending for finalization: 0說明沒有等待終結的物件。

  • jamp -dump:live,format=b,file=./jmap.bin pid。以二進位制格式轉儲java堆到指定路徑下的filename檔案中。指定了live子選項,則只會轉儲活動的物件。

在macOS上使用這個命令同樣也會報錯。但某些命令還是可以的,比如dump二進位制檔案。

1.5 jhat - JVM Heap Dump Browser

作用:與jmap搭配使用,用來分析jmap生成的堆轉儲檔案。jhat內建了一個微型的http/html伺服器,生成dump檔案的分析結果後,可以在瀏覽器中檢視。

不過這個工具一般比較少使用,一是因為功能比較簡陋,VisualVM和MAT等工具完全能夠替代它。二是因為我們一般不會在生產伺服器上直接去dump二進位制檔案,並且分析二進位制檔案是一個比較耗時的工作,所以就沒必要使用命令列工具了。

命令格式:jhat [options] 堆轉儲檔案

引數解釋

第一個引數:option

  • [-stack ]:開關物件分配呼叫棧跟蹤,如果分配位置資訊在堆轉儲中不可用,則必須將此標誌設定為false,預設為true。

  • [-refs ]:開關物件引用跟蹤,預設為true。預設情況下,返回的指標是指向其他特定物件的物件。如果為false則會統計堆中所有物件。

  • [-port ]:設定jhat http server的埠號,預設為7000。

  • [-exclude ]:指定物件查詢時需要排除的資料成員列表檔案。例如,如果檔案列出了java.lang.String.value,那麼當從某個物件Object o計算可達的物件列表時,引用路徑涉及java.lang.String.value的都會排除。

  • [-baseline ]:指定一個基準堆轉儲。在兩個heap dumps中有相同object ID的物件會被標記為不是新的,其他物件會被標記為新的(new),在比較兩個不同的堆轉儲時有用。

  • [-debug ]:設定debug級別,0表示不輸出除錯資訊,值越大表示輸出的除錯資訊越詳細。[0, 1, 2]

  • [-version]:啟動後只顯示版本資訊就退出。

第二個引數:堆轉儲檔案。

命令演示

我們可以先生成一個二進位制檔案。

image-20220317201604307-7519365.png

然後使用jhat命令進行分析

image-20220317202140820-7519702.png

瀏覽器訪問8888埠

image-20220317202228672-7519750.png

1.6 jstack - Stack Trace For Java

作用:檢視或匯出Java應用程式中執行緒堆疊資訊。

執行緒快照是當前Java虛擬機器內每一條執行緒正在執行的方法堆疊的集合。生成執行緒快照的主要目的是定位執行緒出現長時間停頓的原因,比如執行緒死鎖、死迴圈、長時間等待外部資源等。執行緒出現停頓的時候通過jstack來檢視各個執行緒的呼叫堆疊,就可以知道沒有響應的執行緒到底在後臺做什麼事情,或者等待什麼資源。如果Java程式崩潰生成core檔案,jstack工具可以用來獲得core檔案的java stack和native stack的資訊,從而輕鬆地知道Java程式是如何崩潰和在程式何處發生問題。另外jstack工具還可以附屬到正在執行的java程式中,看到當時執行的java程式的java stack和native stack資訊。

命令格式:jstack [options]

引數說明

第一個引數:options

  • -F:當執行緒掛起時,使用jstack -l pid請求不被響應時,強制輸出執行緒堆疊。

  • -l:除堆疊外,顯示關於鎖的附加資訊,比如ownable synchronizers。

  • -m:可以同時輸出java以及C/C++的堆疊資訊。

命令演示:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Demo5_jstack {
    public static void main(String[] args) {
        System.out.println("start");
        test1();
      	// test2();
        System.out.println("end");
    }

    // 測試死迴圈
    private static void test1() {
        while (true) {}
    }

    // 測試死鎖
    private static void test2() {
        Lock lock1 = new ReentrantLock();
        Lock lock2 = new ReentrantLock();

        new Thread(() -> {
            try {
                lock1.lock();
                Thread.sleep(100);
                lock2.lock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread1").start();

        new Thread(() -> {
            try {
                lock2.lock();
                Thread.sleep(100);
                lock1.lock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread2").start();
    }
}

測試死迴圈

啟動程式後,top命令檢視程式狀態

image-20220317215050067-7525051.png

Linux系統中可以使用top -Hp 50242命令檢視程式下的執行緒資訊,但在macOS上不支援這個命令。我也沒找到怎麼檢視macOS裡程式下所有執行緒的方式==。

一般在Linux上的步驟就是下面這幾步:

(1)top檢視哪個程式cpu最高。

(2)top -Hp pid檢視程式下面哪個執行緒cpu最高。

(3)jstack -l pid列印出程式的堆疊資訊,然後將佔有cpu最高的執行緒id轉換為16進位制,將這個16進位制在堆疊資訊中查詢它的位置,一般都能定位到具體的程式碼位置。

image-20220319223044483-7700245.png

測試死鎖

呼叫test2方法,然後啟動程式,使用jstack -l pid命令能夠列印出死鎖資訊。

image-20220319223352905-7700434.png

2、一些視覺化分析工具

2.1 jConsole

使用jConsole可以檢視程式的堆記憶體使用量、執行緒資訊、CPU使用資訊等。

在控制檯輸入jconsole命令,選擇我們本地的程式

image-20220319225123482-7701484.png

進入後就能看到一些基本資訊了

image-20220319225234676-7701555.png

在記憶體模組,我們可以檢視新生代、老年代、元空間等區域的使用率

image-20220319225322645-7701603.png

線上程模組,我們能看到該程式下的所有執行緒,同時還能檢測死鎖

image-20220319225553294.png

image-20220319225626134-7701787.png

在VM概要模組,則可以看到本機的一些JVM資訊。

2.2 Visual VM

作用:是到目前為止隨JDK釋出的功能最強大的執行監視和故障處理程式。官方在VisualVM的軟體說明中寫上了“All-in-One”的描述,說明它除了執行監視、故障處理外,還提供了很多其他方面的功能。如效能分析,VisualVM的效能分析甚至比很多專業的收費工具都好用,而且VisualVM不需要被監視的程式基於特殊的執行,因此它對應用程式的實際效能的影響很小,使得它可以直接應用在生產環境中。

VisualVM基於NetBeans平臺開發,因此它一開始就具備了外掛擴充套件功能的特性,通過外掛擴充套件支援,VisualVM可以做到:

  • 顯示虛擬機器程式以及程式的配置、環境資訊(jps、jinfo)

  • 監視應用程式的CPU、GC、堆、方法區以及執行緒的資訊(jstat、jstack)

  • dump以及分析堆轉儲快照(jmap、jhat)

  • 方法級的程式執行效能分析,找到被呼叫最多、執行時間最長的方法。

  • 離執行緒序快照:收集程式的執行時配置、執行緒dump、記憶體dump等資訊建立一個快照。

  • 動態的安裝plugins。

使用:在控制檯輸入jvisualvm執行即可。進入後主頁還會有一些文件,十分貼心。

image-20220320101903777-7742745.png

在工具欄可以進行外掛的安裝

image-20220320102033691-7742835.png

可以看到左側可以選擇本地程式或者遠端的程式,選擇我們的目標程式,頂部有概述、監視、執行緒、抽樣器、Profiler幾個選項。

image-20220320102219173-7742940.png

監視頁面和jconsole的也有點像,不過在visualvm中可以直接進行堆dump檔案分析

image-20220320102604420-7743165.png

線上程頁面,還可以檢測程式的死鎖,進行執行緒dump的分析

image-20220320102722247-7743243.png

還有很多的功能大家可以一一去看看。以前還不知道JDK自帶了這麼多效能分析利器啊,以後遇到一些效能問題可以嘗試使用一下上面的工具,也不需要額外安裝。

文章首發於我的公眾號【禿頭哥程式設計】,歡迎大家關注。

一些JDK自帶的效能分析利器

相關文章