有時候碰到伺服器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自帶了這麼多效能分析利器啊,以後遇到一些效能問題可以嘗試使用一下上面的工具,也不需要額外安裝。
文章首發於我的公眾號【禿頭哥程式設計】,歡迎大家關注。