文章目錄
JVM - 工慾善其事必先利其器之虛擬機工具(上)
1.虛擬機工具的意義
如果小夥伴們從第一章看到現在,那麼我相信大家對JVM已經有了一定認識了,但是我們也需要學會武裝自己才能夠徹底征服JVM,虛擬機工具自然而然就是最好的武器。
當我們給一個程序系統定位JVM相關問題時,我們對知識的理解就像遊戲裏我們對角色的的理解;我們處理問題的經驗就像我們角色的能力值;數據就像是地圖上所能利用的資源;而工具就是我們通關所運用的手段。
而這裏面的數據就包括:運行日誌
、異常堆棧
、GC日誌
、線程快照
、堆轉儲快照
等。如果能夠合理並且熟練地使用這些虛擬機工具,可以對數據進行快速分析並且提高定位問題解決問題的效率。這裏我們就對一些常用的虛擬機工具進行介紹,讓自己變得更加強大。
2.jps(JVM Process Status Tool)
2.1 一說grep
大家對Linux應該都不會默認,我們經常會使用一個命令就是
ps -ef|grep
。
grep
命令主要就是用於查找,|
是管道命令可以使ps
和grep
同時執行。ps
也是Linux中最常用而且非常強大的查看進程命令,而grep
則是一種十分強大的文本搜索命令,還可以使用正則表達式將匹配的文本進行輸出。假如我們需要查找正在運行的java程序,則可以使用ps -ef|grep java
。
2.2 jps是什麼?
在介紹
JPS
前我們先看一樣東西,就是我們熟悉的JDK
。我們找到JDK
安裝路徑找到bin
目錄,可以看到這裏面有很多應用程序,這其中就包括jps
、jmap
等。
再找到lib
目錄下tools.jar
,打開之後可以看到這裏面其實就包含了我們所看到jps
等命令的源碼,所以JDK
本身其實就提供了許多虛擬機相關的工具來方便我們發現、分析以及解決虛擬機的問題。
jps
就是其中比較典型的JVM工具,我們會發現名稱和ps
命令很相像,而其實功能也與ps
命令相似。我們可以通過jps
命令顯示出虛擬機執行主類(Main Class)名稱以及其進程對應的本地虛擬機標識(Local Virtual Machine Identifier-LVMID)
,雖然功能比較單一但卻是使用頻率最高的工具。
2.3 jps的使用
jps
命令的使用很簡單,這裏我們隨手啓動一個之前的項目,分別介紹一下幾個參數的作用。
jps -l
輸出程序主類全名,若進程執行的是Jar包則輸出Jar包路徑
jps -m
輸出虛擬機進程啓動時傳遞給主類main()函數的參數。
jps -v
輸出虛擬機進程JVM參數,這裏我們可以看到我們之前示例自己所設置的JVM參數。這個命令使用起來也很簡單,相信大家也很熟悉就不過多介紹了。
3.jstat(JVM Statistics Monitoring Tool)
jstat
命令也是我們JDK包中自帶的小工具,主要用於監視虛擬機各種運行狀態信息。可以顯示Java應用程序運行時的類裝載、內存使用、垃圾收集、JIT編譯等運行狀況。若不適用GUI圖形界面工具進的話,那麼它就是定位虛擬機性能問題的首選工具。
3.1 jstat參數介紹
這裏我們輸入jstat -help
查看一下有哪些參數可以供我們使用,另外提一句當大家不知道其他命令如何使用時,一般直接輸入命令例如jstat
或者jstat -help
都會提示相關用法介紹。
- option:參數選項,下面會介紹有哪些參數可以使用
- -t:顯示Timestamp列,用於顯示系統運行時間
- -h:後跟數字,隔幾行顯示標題
- vmid:VM進程ID
- interval:監控執行間隔(單位ms)
- count:監控執行次數(默認循環執行)
上面我們對基本的參數做了一個簡單介紹,這裏我們再通過
jstat -options
來看看options參數能夠怎麼選擇,接下來我們通過實際情況給大家分別介紹下每個參數選項的用法。
3.2 jstat的使用
3.2.1 -class
顯示ClassLoad的裝載、卸載數量以及所佔空間和耗費時間,這裏
8468
就是我們通過jps
獲取的進程pid,250表示250ms執行一次,20表示總共執行次數。
3.2.2 -gc
顯示應用程序GC相關堆信息,主要包括Eden區、Survivor區、老年代、永久代等容量使用情況以及GC時間消耗等信息。
- S0C:年輕代第一個Survivor倖存區的容量
- S1C:年輕代第二個Survivor倖存區的容量
- S0U:年輕代第一個Survivor倖存區的已使用空間大小
- S1U:年輕代第二個Survivor倖存區的已使用空間大小
- EC:年輕代中Eden區的容量
- EU:年輕代中Eden區的已使用空間大小
- OC:老年代容量
- OU:老年代已使用空間大小
- MC:方法區容量
- MU:方法區已使用空間大小
- CCSC:壓縮類空間容量
- CCSU:壓縮類空間已使用大小
- YGC:年輕代垃圾回收次數
- YGCT:年輕代垃圾回收消耗時間
- FGC:老年代垃圾回收次數
- FGCT:老年代垃圾回收消耗時間
- GCT:垃圾回收消耗總時間
3.2.3 -gcnew/-gcold
當然我們也可以通過
-gcnew
和-gcold
單獨查看新生代或者老年代的GC信息。
3.2.4 -gccapacity
顯示各分代容量及使用情況。
- NGCMN:年輕代初始化(最小)容量
- NGCMX:年輕代最大容量
- NGC:年輕代當前容量
- S0C:年輕代第一個Survivor倖存區的容量
- S1C:年輕代第二個Survivor倖存區的容量
- EC:年輕代中Eden區的容量
- OGCMN:老年代初始化(最小)容量
- OGCMX:老年代最大容量
- OGC:老年代當前新生成容量
- OC:老年代容量
- MCMN:元空間初始化(最小)容量
- MCMX:元空間最大容量
- MC:元空間當前新生成容量
- CCSMN:最小壓縮類空間容量
- CCSMX:最大壓縮類空間容量
- CCSC:當前壓縮類空間大小
- YGC:年輕代垃圾回收次數
- FGC:老年代垃圾回收次數
3.2.5 -gcnewcapacity/-gcoldcapacity/-gcmetacapacity
這裏我們也可以通過
-gcnewcapacity
、-gcoldcapacity
、-gcmetacapacity
分別查看年輕代、老年代以及元空間容量以及使用情況。
3.2.5 -gcutil
主要用於顯示GC統計信息。
- S0:年輕代第一個Survivor倖存區已使用容量百分比
- S1:年輕代第二個Survivor倖存區已使用容量百分比
- E:年輕代中Eden已使用容量百分比
- O:老年代已使用容量百分比
- M/P:元空間(JDK1.8以前Perm永久代)已使用容量百分比
- CCS:壓縮類已使用容量百分比
- YGC:年輕代垃圾回收次數
- YGCT:年輕代垃圾回收消耗時間
- FGC:老年代垃圾回收次數
- FGCT:老年代垃圾回收消耗時間
- GCT:垃圾回收消耗總時間
3.2.6 -gccause
內容和
-gcutil
大致一樣,顯示GC相關信息,會額外顯示最後一次或正在進行的GC原因。
3.2.7 -compiler
顯示JIT編譯相關信息。
- Compiled:編譯任務執行數量
- Failed:編譯任務執行失敗數量
- Invalid:編譯任務執行失效數量
- Time:編譯任務消耗時間
- FailedType:最後一個編譯失敗任務類型
- FailedMethod:最後一個編譯失敗任務信息
3.2.7 -printcompilation
顯示已經被JIT編譯的方法信息。
- Compiled:編譯任務數量
- Size:方法生成字節碼大小
- Type:編譯類型
- Method:方法標識(類名、方法名)
3.2.8 -class
顯示加載Class相關信息。
- Loaded:已裝載類數量
- Bytes:裝載類所佔字節數
- Unloaded:已卸載類數量
- Bytes:卸載類所佔字節數
- Time:裝載以及卸載類所耗時間
4.jinfo(Configuration Info for Java)
jinfo
命令的作用主要是實時查看虛擬機格箱參數。我們知道使用jps -v
命令可以查看虛擬機啓動時顯示指定的參數列表,但是我們想要知道除了顯示指定的其他參數要如何做呢?jinfo
就給我們提供了這個功能。
這裏我們通過jinfo pid
可以看到經過一個短時間的等待後,會將很多很多信息輸出給我們,這裏面就包括各種應用環境配置、JVM參數配置等,小夥伴們可以自己動手去執行看看。如果大家想要查看程序詳細的配置,那麼jinfo
命令是你的不二選擇。
5.jmap(Memory Map for Java)
5.1 jmap是什麼?
jmap
命令主要可以用於生成堆轉儲快照。和jinfo
命令一樣,如果大家在windows平臺執行某些命令失效是正常的,有些功能在Windows下是不支持的。
除了可以獲取dump文件,我們還能夠通過jmap
查詢到堆中各代空間使用率等情況,並且可以查看到每種類的實例、空間佔用等信息。
另外我們除了jmap
生成堆轉儲快照之外,還可以通過JVM啓動參數中設置-XX:+HeapDumpOnOutOfMemoryError
讓程序在OOM後自動生成dump文件。然後通過分析dump文件去解決相關問題。
5.2 jmap常用指令
5.2.1 -heap
顯示堆中詳細信息,包括參數配置、使用垃圾收集器、各代空間使用等情況。
5.2.2 -histo
-histo
命令是我們上章使用過的一個命令,大家可能會有一點印象。當時我們通過jmap -histo:live 2772>jmap_histo.log
命令將堆中存活對象統計信息輸出到了日誌文件中用於分析開啓逃逸分析的效果。這裏我們也可以通過jmap -histo pid
直接去查看堆中對象信息,另外如果因爲信息太多的我們可以通過jmap -histo pid|more
自己去分頁查看。
這裏面主要包括類、實例數量、所佔字節量。另外我們可以看到其中有幾個比較特殊的標識:【I、【B、【C,這幾個其實就是我們所熟悉的int、byte、char類型數據。
5.2.3 -dump
dump
命令主要作用就是用來生成堆轉儲快照,可供我們對程序運行情況進行分析。主要格式就是jmap -dump:format=b,file=xxx pid
5.3 安利一個JVM可視化分析網站-PerfMa
我們在之前介紹了很多例如GC日誌、JVM啓動參數、堆轉儲快照等,那麼這些日誌怎麼分析、參數怎麼設置呢?
除了我們通過自己的經驗之外,這裏我給大家安利一個網站【PerfMa】。
這個網站提供了很多可視化分析的界面,並且還能夠通過你不同機器的硬件配置情況制定不同的JVM啓動參數。這裏我們就用我們上面dump
下的堆轉儲快照爲例。
這裏導入dump文件也十分方便,直接拖拽即可。
大家可以看到分析生成後的可視化界面真的十分強大,堆內存使用情況、GC ROOT個數、線程個數以及每個類的實例個數、所佔容量大小甚至類加載器、每個對象信息都能夠看到,還支持一些常用的條件搜索可以定位到我們需要精準查找的內容。
大家可以自己使用感受一下他的強大。如果我們自身已經對JVM有了一定的瞭解,那麼這些強大的可視化分析工具就不是阻礙我們成長的羈絆而是助力器。
5.jhat(JVM Heap Analysis Tool)
當然如果我們沒有上面介紹的可視化分析工具的話,我們要如何去分析dump文件呢?答案就是
jhat
命令,這個命令經常和jmap
搭配使用,主要作用和上面類似就是用於分析堆轉儲快照。
jhat
可以將dump文件進行分析後通過瀏覽器去進行查看,但是需要注意的是jhat
分析工作是一個耗時且對硬件資源有消耗的過程,整個分析功能也比較簡陋,不過我們這裏也來看看他到底是會怎樣進行分析。
我們使用上面的dump文件。
如果顯示上面這樣就是啓動成功了,我們可以看到他提示默認端口爲7000,這裏我們嘗試進行訪問。
我們可以看到jhat
會通過分析dump文件幫我們生成一個這樣的界面,這裏面也包括一些堆使用情況,這些數據大家是不是十分熟悉。另外jhat
還提供了一個OQL查詢功能。
這裏我們可以通過OQL語句去按照條件查詢對象情況,如果大家想要嘗試的話可以看看這裏也提供了一個OQL HELP
,不過如果我們已經有了更強大的工具的話可能就很少會去使用這些命令了。
6.jstack(Stack Trace for Java)
6.1 jstack是什麼?
我們上面講的那麼多命令,會發現很多都是和堆內存信息有關的信息,那麼在我們項目中除了堆中內存管理還有什麼是我們經常會碰到並且總是難以下手的呢?沒錯,就是線程。
jstack
命令就是用於生成JVM當前時刻的線程快照(threaddump)。線程快照其實就是當前JVM中每條線程正在執行的方法堆棧的一個集合。
&ems;當線程死鎖、死循環、請求外部資源長時間等待時都肯恩改造成線程長時間停頓,我們生成線程快照的目的就是爲了定位並解決這些問題。當線程出現停頓時我們可以通過jstack
來查看各線程調用堆棧鏈,從而分析線程當前狀態以及造成問題的原因。
6.2 jstack的使用
同樣我們啓動開始的程序,通過
jps
命令獲取進程PID。這裏我們通過jstack -l 4728
命令就會輸出當前時刻線程快照了,我們可以看到這裏麪包括每個線程的狀態、標識信息、堆棧調用鏈等,還是很詳細的,我們可以通過這些信息去分析線程停頓造成的原因從而找到解決方案。下面我們來一個示例看看當我們線程死鎖了會是怎樣的。
public class DeadLock {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock1){
try {
System.out.println(Thread.currentThread().getName() + "get lock1");
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
synchronized (lock2){
System.out.println(Thread.currentThread().getName() + "get lock2");
}
}
}, "線程1").start();
new Thread(() -> {
synchronized (lock2){
try {
System.out.println(Thread.currentThread().getName() + "get lock2");
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
synchronized (lock1){
System.out.println(Thread.currentThread().getName() + "get lock1");
}
}
}, "線程2").start();
}
}
線程死鎖是指由於兩個或者多個線程互相持有對方所需要的資源,導致這些線程處於等待狀態,無法前往執行
大家應該都知道線程是什麼,就是兩個或多個線程互相持有對方所需要的資源,導致這些線程都處於一個等待狀態,從而造成線程死鎖。
我們這裏用這個簡單的例子來模擬一下線程死鎖,我們啓動之後會發現應用處於等待狀態,這個時候我們通過開始的命令jstack -l pid
。
這裏可以看到他幫我們分析出了發現了一處死鎖,並且把相關的線程信息和方法調用堆棧位置都給我們標記出來了,同時還顯示出了死鎖造成的原因是由於等待哪一個鎖造成的。通過線程快照我們可以很快地定位線程停頓的原因,大家可以自己動手去試一試感受一下。
6.3 線程狀態
這裏提到線程那就大家一起簡單複習一下線程有哪幾種狀態。
- NEW:線程創建狀態。
- RUNNABLE:線程執行狀態。
- BLOCKED :線程阻塞狀態。一般情況下是線程正在等待獲取一個鎖,像我們上面死鎖的示例就是,在未獲取鎖之前都會是該狀態。如果長時間處於該狀態則需要考慮是否死鎖。
- WAITING :線程等待狀態。當我們平時執行了
Object.wait()
或Thread.join()
都會使線程變爲該狀態,直到另一個線程執行Object.notify()
等相關代碼時纔會被喚醒,這種狀態有意而爲的話是可以沒有時間限制的。- TIMED_WAITING :線程有限等待狀態。上面的
WAITING
的話可以理解爲是沒有時間限制的等待,只有符合某種條件喚醒纔會繼續執行;而該狀態等待一定時間後會主動去喚醒線程獲取資源。- TERMINATED:線程結束狀態。
這裏我們主要就是稍微理解一下
WAITING
和BLOCKED
,前者是主動顯示申請阻塞,後者屬於被動阻塞。另外一個就是WAITING
和TIMED_WAITING
,前者可以無限期等待而後者有一個時間限制。
7.總結
這一章我們主要介紹了JDK自帶工具包中一些關於JVM應用程序相關的命令,可以幫助我們去查看應用程序的一些堆存儲狀態、應用程序信息、線程狀態等,讓我們可以在有問題或者需要時對其運行狀況進行了解。
隨着JAVA的發展,也有越來越多更成熟更好用的可視化工具可以幫助我們對應用程序進行分析,下一章我們就來對這些可視化工具進行一個瞭解和使用。