一些JDK自帶的性能分析利器
有時候碰到服務器CPU飆升或者程序卡死之類的問題,一般都不太好定位。這類bug一般都隱藏的比較深並且還可能是偶發性的,比較棘手。
對於此類問題,一般我們都有固定的分析流程。藉助於JDK自帶的一些分析工具,比如jstack、jmap、jstat一類的命令行工具,除此之外,還有jconsole、mat、jvisualvm這些圖形界面分析工具。
這篇文章基於JDK8,操作系統是macOS 12.0.1
1、一些命令行分析工具
這些命令行分析工具都在jdk/bin目錄下
解壓jdk/lib/tool.jar可以得到上述工具的class文件
1.1 jps – JVM Process Status Tool
作用:列出正在運行的虛擬機進程,並顯示虛擬機執行主類名稱以及這些進程的本地虛擬機唯一ID。
第一個參數說明:
-
-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
然後啟動demo
剛啟動時,程序會申請2M的數組,可以看到eden區的使用率是62.37%,ygc的值為0。
讓程序申請第二個2M的數組,之後再查看堆內存信息
可以發現eden區的使用率達到了87.37%。
讓程序申請第三個2M的數組,發現控制台輸出提示
再查看堆內存信息
發現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鏈接
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]:啟動後只顯示版本信息就退出。
第二個參數:堆轉儲文件。
命令演示:
我們可以先生成一個二進制文件。
然後使用jhat命令進行分析
瀏覽器訪問8888端口
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
命令查看進程狀態
Linux系統中可以使用top -Hp 50242
命令查看進程下的線程信息,但在macOS上不支持這個命令。我也沒找到怎麼查看macOS里進程下所有線程的方式==。
一般在Linux上的步驟就是下面這幾步:
(1)top
查看哪個進程cpu最高。
(2)top -Hp pid
查看進程下面哪個線程cpu最高。
(3)jstack -l pid
打印出進程的堆棧信息,然後將佔有cpu最高的線程id轉換為16進制,將這個16進制在堆棧信息中查詢它的位置,一般都能定位到具體的代碼位置。
測試死鎖
調用test2方法,然後啟動程序,使用jstack -l pid
命令能夠打印出死鎖信息。
2、一些可視化分析工具
2.1 jConsole
使用jConsole可以查看程序的堆內存使用量、線程信息、CPU使用信息等。
在控制台輸入jconsole
命令,選擇我們本地的程序
進入後就能看到一些基本信息了
在內存模塊,我們可以查看新生代、老年代、元空間等區域的使用率
在線程模塊,我們能看到該進程下的所有線程,同時還能檢測死鎖
在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
執行即可。進入後主頁還會有一些文檔,十分貼心。
在工具欄可以進行插件的安裝
可以看到左側可以選擇本地進程或者遠程的進程,選擇我們的目標程序,頂部有概述、監視、線程、抽樣器、Profiler幾個選項。
監視頁面和jconsole的也有點像,不過在visualvm中可以直接進行堆dump文件分析
在線程頁面,還可以檢測程序的死鎖,進行線程dump的分析
還有很多的功能大家可以一一去看看。以前還不知道JDK自帶了這麼多性能分析利器啊,以後遇到一些性能問題可以嘗試使用一下上面的工具,也不需要額外安裝。
文章首發於我的公眾號【禿頭哥編程】,歡迎大家關注。