1.概述
給一個(gè)系統(tǒng)定位問(wèn)題的時(shí)候,知識(shí)、經(jīng)驗(yàn)是關(guān)鍵基礎(chǔ),數(shù)據(jù)是依據(jù),工具是運(yùn)用知識(shí)處理數(shù)據(jù)的手段。這里所說(shuō)的數(shù)據(jù)包括:運(yùn)行日志、異常堆棧、GC日志、線程快照(threaddump/javacore文件)、堆轉(zhuǎn)儲(chǔ)快照(heapdump/hprof文件)等。
2.JDK的命令行工具
| 名稱(chēng) | 主要作用 |
|---|---|
| jps | JVM Process Status Tool,顯示指定系統(tǒng)內(nèi)所有的HotSpot虛擬機(jī)進(jìn)程 |
| jstat | JVM Statistics Monitoring Tool,用于收集HotSpot虛擬機(jī)各方面的運(yùn)行數(shù)據(jù) |
| jinfo | Configuration Info for Java,顯示虛擬機(jī)配置信息 |
| jmap | Memory Map for Java,生成虛擬機(jī)的內(nèi)存轉(zhuǎn)儲(chǔ)快照(heapdump文件) |
| jhat | JVM Heap Dump Browser,用于分析和heapdump文件 |
| jstack | Stack Trace for Java,顯示虛擬機(jī)的線程快照 |
2.1.虛擬機(jī)進(jìn)程狀況工具:jps
功能:可以列出正在運(yùn)行的虛擬機(jī)進(jìn)程,并顯示虛擬機(jī)執(zhí)行主類(lèi)(Main Class,main()函數(shù)所在的類(lèi))名稱(chēng)以及這些進(jìn)程的本地虛擬機(jī)唯一ID(Local Virtual Machine Identifier,LVMID)。
命令格式:jps [options] [hostid]
| 選項(xiàng) | 作用 |
|---|---|
| -q | 只輸出LVMID,省略主類(lèi)的名稱(chēng) |
| -m | 輸出虛擬機(jī)進(jìn)程啟動(dòng)時(shí)傳遞給主類(lèi)main函數(shù)的參數(shù) |
| -l | 輸出主類(lèi)的全名,如果進(jìn)程執(zhí)行的是Jar包,輸出Jar路徑 |
| -v | 輸出虛擬機(jī)進(jìn)程啟動(dòng)時(shí)JVM參數(shù) |
2.2.虛擬機(jī)統(tǒng)計(jì)信息監(jiān)視工具:jstat
功能:可以顯示本地或者遠(yuǎn)程虛擬機(jī)進(jìn)程中的類(lèi)加載、內(nèi)存、垃圾收集、JIT編譯等運(yùn)行數(shù)據(jù)。
命令格式:jstat -<optios> [-t] [-h<Lines>] <vmid> [ <interval> [ <count> ] ]
- 對(duì)于命令格式中的VMID與LVMID需要特別說(shuō)明一下:如果是本地虛擬機(jī)進(jìn)程,VMID與LVMID是一致的;如果是遠(yuǎn)程虛擬機(jī)進(jìn)程,那么VMID的格式應(yīng)當(dāng)是:
[protocol:][//]lvmid[@hostname[:port]/servername] - -t:可以在打印的列加上Timstamp列,用于顯示系統(tǒng)的運(yùn)行時(shí)間
- -h:可以在指定輸出N行后輸出一次表頭
- 參數(shù)interval和count代表查詢間隔和次數(shù),如果省略了這兩個(gè)參數(shù),說(shuō)明只查詢一次。
| 選項(xiàng) | 作用 |
|---|---|
| -class | 監(jiān)視類(lèi)裝載、卸載數(shù)量、總空間以及類(lèi)裝載所耗費(fèi)的時(shí)間 |
| -gc | 監(jiān)視Java堆狀況、包括Eden區(qū)、兩個(gè)Survivor區(qū)、老年代、永久代等的容量、已用空間、GC時(shí)間合計(jì)等信息 |
| -gccapacity | 監(jiān)視內(nèi)容與-gc基本相同,但輸出主要關(guān)注Java堆各個(gè)區(qū)域使用到的最大和最小空間 |
| -gcutil | 監(jiān)視內(nèi)容與-gc基本相同,但輸出主要關(guān)注已使用空間占總空間的百分比 |
| -gccause | 與-gcutil功能一樣,但是會(huì)額外輸出導(dǎo)致上一次GC產(chǎn)生的原因 |
| -gcnew | 監(jiān)視新生代GC狀況 |
| -gcnewcapacity | 監(jiān)視內(nèi)容與-gcnew基本相同,但輸出主要關(guān)注使用到的最大和最小空間 |
| -gcold | 監(jiān)視老年代GC狀況 |
| -gcoldcapacity | 監(jiān)視內(nèi)容與-gcold基本相同,但輸出主要關(guān)注使用到的最大和最小空間 |
| -gcpermcapacity | 輸出永久代使用到的最大和最小空間(JDK1.8以后被-gcmetacapacity輸出元數(shù)據(jù)空間使用到的最大和最小空間取代) |
| -compiler | 輸出JIT編譯器編譯過(guò)的方法、耗時(shí)等信息 |
| -printcompilation | 輸出已經(jīng)被JIT編譯過(guò)的方法 |
2.3.Java配置信息工具:jinfo
功能:實(shí)時(shí)地查看和調(diào)整虛擬機(jī)各項(xiàng)參數(shù)
命令格式:jinfo [option] pid
| 選項(xiàng) | 作用 |
|---|---|
| -flag<name> | 打印指定的JVM參數(shù)值,例如:jinfo SurvivorRatio 8232 |
| -flag[+/-]<name> | 使指定的JVM參數(shù)生效或失效,例如:jinfo -flag -printGCDateStamps 8232 |
| -flag<name>=<value> | 為指定的VM參數(shù)設(shè)定指定的值,例如:jinfo -flag MaxHeapFreeRatio=80 8232 |
| -flags | 打印所有的VM參數(shù),例如:jinfo -flags 8232 |
| -sysprops | 打印系統(tǒng)參數(shù),例如 jinfo -sysprops 8232 |
2.4.Java內(nèi)存映像工具:jmap
功能:可用于生成堆轉(zhuǎn)儲(chǔ)快照(heapdump或dump文件),也可用于查詢finalize執(zhí)行隊(duì)列、Java堆和永久代的詳細(xì)信息,如空間使用率、當(dāng)前用的是哪種收集器等。
命令格式:jmap [option] vmid
| 選項(xiàng) | 作用 |
|---|---|
| -dump | 生成Java堆轉(zhuǎn)儲(chǔ)快照。格式為:-dump:[live, ]format=b,file=<filename>,其中l(wèi)ive自參數(shù)說(shuō)明是否只dump出存活的對(duì)象。例如:jmap -dump:format=b,file=eclipse.bin 3500 |
| -finalizerinfo | 顯示在F-Queue中等待Finalizer線程執(zhí)行finalize方法的對(duì)象。只在Linux/Solaris平臺(tái)下有效 |
| -heap | 顯示Java堆詳細(xì)信息,如使用哪種回收器、參數(shù)配置、分代狀況等。只在Linux/Solaris平臺(tái)下有效 |
| -histo | 顯示堆中對(duì)象統(tǒng)計(jì)信息,包括類(lèi)、實(shí)例數(shù)量、合計(jì)容量 |
| -permstat | 以ClassLoader為統(tǒng)計(jì)口徑顯示永久代內(nèi)存狀態(tài)。只在Linux/Solaris平臺(tái)下有效 |
| -F | 當(dāng)虛擬機(jī)進(jìn)程對(duì)-dump選項(xiàng)沒(méi)有響應(yīng)時(shí),可使用這個(gè)選項(xiàng)強(qiáng)制生成dump快照。只在Linux/Solaris平臺(tái)下有效 |
2.5.虛擬機(jī)堆轉(zhuǎn)儲(chǔ)快照分析工具:jhat
功能:jhat(JVM Heap Analysis Tool)用于分析jmap生成的堆轉(zhuǎn)儲(chǔ)快照文件。
命令格式:jhat <filename>
2.6.Java堆棧跟蹤工具:jstack
功能:用于生成虛擬機(jī)當(dāng)前時(shí)刻的線程快照(一般稱(chēng)為threaddump或者javacore文件)。線程快照就是當(dāng)前虛擬機(jī)內(nèi)每一條線程正在執(zhí)行的方法堆棧的集合,生成線程快照的主要目的是定位線程長(zhǎng)時(shí)間停頓的原因,如線程間死鎖、死循環(huán)、請(qǐng)求外部資源導(dǎo)致的長(zhǎng)時(shí)間等待等都是導(dǎo)致線程長(zhǎng)時(shí)間停頓的常見(jiàn)原因。
命令格式:jstack [option] vmid
| 選項(xiàng) | 作用 |
|---|---|
| -F | 當(dāng)正常輸出的請(qǐng)求不被響應(yīng)時(shí),強(qiáng)制輸出線程堆棧 |
| -l | 除堆棧外,顯示關(guān)于鎖的附加信息 |
| -m | 如果調(diào)用到本地方法的話,可以顯示C/C++的堆棧 |
3.JDK的可視化工具
3.1.Java監(jiān)視與管理控制臺(tái):JConsole
JConsole(Java Monitoring and Management Console)是一種基于JMX的可視化監(jiān)視、管理工具。
3.1.1.啟動(dòng)JConsole
通過(guò)JDK/bin目錄下的jconsole.exe啟動(dòng)JConsole后,將自動(dòng)搜索出本機(jī)運(yùn)行的所有虛擬機(jī)進(jìn)程,不需要用戶自己再使用jps來(lái)查詢了。


如上圖所示,主界面共包括“概述”、“內(nèi)存”、“線程”、“類(lèi)”、“VM概要”和“MBean”6個(gè)頁(yè)簽。
3.1.2.內(nèi)存監(jiān)控
“內(nèi)存”頁(yè)簽相當(dāng)于可視化的jstat命令,用于監(jiān)視受收集器管理的虛擬機(jī)內(nèi)存(Java堆、永久代/元空間)的變化趨勢(shì)。我們通過(guò)下面的一段代碼來(lái)演示一下JConsole是如何監(jiān)控內(nèi)存變化的。
package com.nwpu.davince.jvm;
import java.util.ArrayList;
import java.util.List;
public class TestDemo {
static class OOMObject {
public byte[] placeHolder = new byte[64 * 1024];
}
private static void fillHeap(int num) throws InterruptedException {
List<OOMObject> list = new ArrayList<TestDemo.OOMObject>();
for (int i = 0; i < num; i++) {
Thread.sleep(50);
list.add(new OOMObject());
}
System.gc();
}
public static void main(String[] args) throws InterruptedException {
fillHeap(1000);
}
}

程序運(yùn)行后,可以看到內(nèi)存池Eden區(qū)的運(yùn)行趨勢(shì)呈現(xiàn)折線狀。而監(jiān)視范圍擴(kuò)大至整個(gè)堆后,會(huì)發(fā)現(xiàn)曲線是一條向上增長(zhǎng)的平滑曲線。

3.1.3.線程監(jiān)控
“線程”頁(yè)簽的功能相當(dāng)于可視化的jstack命令,遇到線程停頓時(shí)可以使用這個(gè)頁(yè)簽進(jìn)行監(jiān)控分析。線程停頓的主要原因有:等待外部資源(數(shù)據(jù)庫(kù)連接、網(wǎng)絡(luò)資源、設(shè)備資源等)、死循環(huán)、鎖等待(活鎖和死鎖)。我們將通過(guò)以下代碼來(lái)分別演示這幾種情況。
package com.nwpu.davince.jvm;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class JStackDemo {
/**
* 線程死循環(huán)
*/
public static void createBusyThread() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true)
;
}
}, "testBusyThread");
thread.start();
}
/**
* 線程鎖等待
*/
public static void createLockThread(final Object lock) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "testLockThread");
thread.start();
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
br.readLine();
createBusyThread();
br.readLine();
Object lock = new Object();
createLockThread(lock);
}
}
程序運(yùn)行后,首先在“線程”頁(yè)簽中選擇main線程。堆棧追蹤顯示BufferReader在readBytes方法中等待System.in的鍵盤(pán)輸入,這是線程為Runnable狀態(tài),Runnable狀態(tài)的線程會(huì)被分配CPU運(yùn)行時(shí)間,但readBytes方法檢查到流沒(méi)有更新時(shí)會(huì)立刻歸還執(zhí)行令牌,這種等待只消耗很小的CPU資源。

接著監(jiān)控testBusyThread線程,testBusyThread線程一直在執(zhí)行空循環(huán),從堆棧追蹤中看到一直在JStackDemo.java代碼的16行停留,16行行為:while(true)。這時(shí)線程為Runnable狀態(tài),而且沒(méi)有歸還線程執(zhí)行令牌的動(dòng)作,會(huì)在空循環(huán)上用盡全部CPU執(zhí)行時(shí)間直到線程切換,這種等待會(huì)消耗較多的CPU資源。

testLockThread線程在等待著lock對(duì)象的notify或notifyAll方法的出現(xiàn),線程這時(shí)候處于WAITING狀態(tài),在被喚醒前不會(huì)被分配執(zhí)行時(shí)間。testLockThread線程在正處于正常的活鎖等待,只要lock對(duì)象的notify或notifyAll方法被調(diào)用,這個(gè)線程便能激活以繼續(xù)執(zhí)行。

最后,我們來(lái)演示一個(gè)無(wú)法再被激活的死鎖等待的例子。
package com.nwpu.davince.jvm;
public class ThreadDeadLock {
static class SynAddRunnable implements Runnable {
int a, b;
public SynAddRunnable(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
synchronized (Integer.valueOf(a)) {
synchronized (Integer.valueOf(b)) {
System.out.println(a + b);
}
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new SynAddRunnable(1,2)).start();
new Thread(new SynAddRunnable(2,1)).start();
}
}
}
上述程序?qū)?huì)出現(xiàn)死鎖等待的情況。而造成死鎖的原因是Integer.valueOf()方法基于減少對(duì)象創(chuàng)建次數(shù)和節(jié)約內(nèi)存的考慮,[-128,127]之間的數(shù)字會(huì)被緩存,當(dāng)valueOf()方法傳入這個(gè)參數(shù)在這個(gè)范圍之內(nèi),將直接返回緩存中的對(duì)象。也就是說(shuō),代碼中調(diào)用了200次Integer.valueOf()方法一共就只返回了兩個(gè)不同的對(duì)象。假如某個(gè)線程的兩個(gè)synchronized塊之間發(fā)生了一次線程切換,那就會(huì)出現(xiàn)線程A等著被線程B持有的Integer.valueOf(1),而線程B又等著被線程A持有的Integer.valueOf(2),結(jié)果就出現(xiàn)了死鎖等待。


3.2.多合一故障處理工具:VisualVM
3.3.內(nèi)存分析工具:MAT(Eclipse Memory Analyzer Tool)