手把手教你使用JConsole
目前市面上有多種 JVM 監(jiān)控工具供我們選擇,其中 JConsole 可以算是 JVM 監(jiān)控始祖了,從 JDK1.5 就開始引入。最關(guān)鍵的是 JConsole 是 JDK 官方自帶的控制臺(tái),在一些特殊環(huán)境,比如網(wǎng)絡(luò)不通的情況下,可能是你唯一可以使用的圖形化監(jiān)控工具了。因此我們有必要對(duì) JConsole 的使用方法做一個(gè)了解。并且對(duì)大多數(shù)監(jiān)控需求來說,JConsole 完全可以滿足我們的需要,今天我們就來看看一直被我們低估的 JConsole 到底如何使用。
首先我們先找到 JDK/bin 目錄下的 JConsole 命令,啟動(dòng) JConsole。下圖是我本機(jī)的 JConsole 啟動(dòng)界面。
圖片
我們可以看到,JConsole 啟動(dòng)時(shí)在本地進(jìn)程中會(huì)列出所有 JVM 進(jìn)程 PID,相當(dāng)于可視化的 jps 命令。我本機(jī)現(xiàn)在運(yùn)行的幾個(gè)虛擬機(jī)進(jìn)程分別是 StudyDemoApplication、JConsole、jps 和 idea。現(xiàn)在我們雙擊 StudyDemoApplication 進(jìn)去看看。
進(jìn)入具體的進(jìn)程后,我們會(huì)看到幾個(gè) tab 選項(xiàng),其中概覽是我們需要重點(diǎn)關(guān)注的,概覽的內(nèi)容比較直觀,包括了我們最關(guān)心的堆內(nèi)存使用量、線程、類、CPU 使用情況這四個(gè)信息的曲線圖。除了概覽,被高頻使用的還有內(nèi)存和線程 2 個(gè) tab,下面我們分別介紹它們。
圖片
內(nèi)存
我們先來看內(nèi)存 tab 吧。 內(nèi)存這個(gè) tab 用于監(jiān)視虛擬機(jī)堆內(nèi)存、非堆內(nèi)存、內(nèi)存池等的變化趨勢(shì)。可以通過圖表下拉框去選擇要監(jiān)視的信息,還可以選擇時(shí)間范圍。
之前我在《 如何 使用 JVM 工具排查線上問題》課中介紹過 jstat 的使用方法,JConsole 內(nèi)部集成了 jstat,圖形化的展示讓內(nèi)存信息更直觀。注意,這里的下拉列表和你使用的垃圾收集器有關(guān),比如默認(rèn)使用的是 Parallel Scavenge 垃圾收集器,縮寫就是 PS,因此會(huì)看到 PS 前綴。
我們通過一個(gè)具體的例子來感受一下。下面這段代碼每隔 30 毫秒都會(huì)往 list 追加大小為 64KB 的 OOMObject 對(duì)象,此時(shí)我的堆設(shè)置是 100MB。
public class TestMemoryMonitor {
/**
* 存占位符對(duì)象,一個(gè) OOMObject 大約占 64KB
*/
staticclass OOMObject{
publicbyte[] placeholder = newbyte[64 * 1024];
}
public static void fillHeap(int num) throws InterruptedException {
List<OOMObject> list = new ArrayList<>();
for (int i = 0; i < num; i++) {
//稍作延時(shí),令監(jiān)視曲線的變化更加明顯
Thread.sleep(30);
list.add(new OOMObject());
}
System.gc();
}
public static void main(String[] args) throws Exception {
fillHeap(3000);
}
}
程序運(yùn)行后,連接一下運(yùn)行的 PID。在圖表列表中,選擇 內(nèi)存池“PS Eden Space”,我們一起來看下內(nèi)存的使用情況。
圖片
我們看到 for 循環(huán) 1000 次執(zhí)行完成后,形成了一個(gè)折線狀的圖。你可以看一下,右下角柱狀圖中的【堆】區(qū)域有 3 個(gè)柱狀圖,從左到右依次是“PS Old Gen”、“PS Eden Space”和“PS Survivor Space”。
我們可以看到在開始往堆填充數(shù)據(jù)時(shí),出現(xiàn)了一條勻速向上的、趨近直線的折線。每隔 30 毫秒停止填充,曲線急劇下降,周而復(fù)始直至完成。我們?cè)賮砜纯磮D表右下角的柱狀圖,當(dāng) for 循環(huán)執(zhí)行結(jié)束,我們執(zhí)行 System.gc() 后,Eden 和 Survivor 區(qū)中的空間幾乎都被釋放了,但是老年代的已使用空間還在緩慢增長(zhǎng)。
顯然,我們的對(duì)象并沒有真正被釋放,而是在 System.gc() 發(fā)生之后進(jìn)入了老年代。在我們的代碼中 List 在整個(gè) fillHeap 方法中都是有效的,當(dāng)我們執(zhí)行 System.gc() 時(shí)并沒有離開 List 的作用域。
如果這個(gè)時(shí)候想要回收全部?jī)?nèi)存該怎么辦呢?你可以嘗試一下,把 System.gc() 放到 fillHeap 方法外運(yùn)行一下試試,顯然由于此時(shí) fillHeap 方法已經(jīng)退出,List 不再有效,隨時(shí)可以被回收。
在實(shí)驗(yàn)的時(shí)候要注意 System.gc() 并不一定總是有效的。這是因?yàn)?System.gc() 并不是強(qiáng)制執(zhí)行的,而只是通知 JVM 快去回收對(duì)象,具體什么時(shí)候執(zhí)行 JVM 說了算。這種感覺就像你在餐廳點(diǎn)菜,你覺得上菜比較慢,你就會(huì)催促服務(wù)員,但是催促服務(wù)員并不意味著立刻就會(huì)上菜,具體菜什么時(shí)候做好還是廚師說了算。
線程
接下來,我們聊聊另一個(gè)重要的 tab——線程。我在《 如何 使用 JVM 工具排查線上問題》課程中介紹過 jstack 命令,線程 tab 的功能基本和 jstack 命令一致。我們知道線程出現(xiàn)阻塞的原因包括: 等待外部資源、長(zhǎng)時(shí)間執(zhí)行的循環(huán)、鎖。
下面我們通過一段代碼來觀測(cè)常見的線程阻塞例子。
public static void createBusyThread(){
Thread test1= new Thread(()->{while(true);});
test1. start();
}
public static void createLockThread (final Object lock) {
Thread test2= new Thread (()->{
synchronized(lock){
try{
lock. wait();
}catch(InterruptedException e){
}
}});
test2. start();
}
public static void main (String[] args) throws Exception {
BufferedReader br= new BufferedReader( new InputStreamReader(System. in));
br.readLine();
createBusyThread();
br.readLine();
Object obj= new Object();
createLockThread(obj);
}
監(jiān)控線程的Runnable 狀態(tài)
我們?cè)诰€程 tab 中可以看到 main 線程,你可以看一下圖片。右側(cè)堆棧顯示 readBytes 在參數(shù)輸入,此時(shí)線程還是 Runnable 狀態(tài),Runnable 意味著線程會(huì)被分配 CPU 時(shí)間片,readBytes 發(fā)現(xiàn)沒有任何輸入又會(huì)歸還 CPU 時(shí)間片,這種性能消耗幾乎可以忽略不計(jì)。
圖片
接著監(jiān)控 test1 線程,test1 線程執(zhí)行的是自旋操作,從右側(cè)的堆棧中我們可以發(fā)現(xiàn)線程此時(shí)在 MonitoringTest.java 代碼的 41 行,而第 41 行就是死循環(huán)。顯然這種自旋會(huì)極大浪費(fèi) CPU 資源。
圖片
監(jiān)控線程的 WAITING 狀態(tài)
下面我們?cè)賮砜纯?test2 線程,圖片顯示 test2 線程在等待某個(gè)鎖,線程這時(shí)候處于 WAITING 狀態(tài)。test2 線程處于一個(gè)正常的狀態(tài),它在等待一個(gè)鎖,直到鎖對(duì)象被 notify 調(diào)用才會(huì)繼續(xù)執(zhí)行。
圖片
監(jiān)控線程的 BLOCKED 狀態(tài)(死鎖)
下面我們看一個(gè)死鎖的例子。出現(xiàn)線程死鎖之后,我們可以看到一個(gè)新的“死鎖”tab。
圖片
圖中我們可以很清晰地看到 43 號(hào)線程在等待一個(gè) Integer 對(duì)象,這個(gè)對(duì)象被 12 號(hào)線程持有。而點(diǎn)擊 12 號(hào)線程則顯示它也在等待一個(gè) Integer 對(duì)象,這個(gè)對(duì)象被 43 號(hào)線程持有,43 號(hào)線程和 12 號(hào)線程形成了死鎖,是不是很直觀?
總結(jié)
以上就是我們講的 JConsole 的常用用法,可以看到,我們?nèi)粘5谋O(jiān)控、死鎖判斷、內(nèi)存排查都可以使用 JConsole 去排查。在沒有圖形化的生產(chǎn)環(huán)境可以使用 JConsole 遠(yuǎn)程連接。對(duì) JConsole 的使用大多數(shù)是線上問題診斷。既然要診斷,最重要的還是基于我們的研發(fā)經(jīng)驗(yàn),JConsole 只是工具而已,不能本末倒置,認(rèn)為有一個(gè)工具就可以解決問題了,更需要的是我們?nèi)藶榈呐袛唷?/span>
此外,我們通過線程 tab 看到了線程的 Runnable、WAITING 以及 BLOCKED 3 種狀態(tài)。要特別注意區(qū)分 WAITING 和 BLOCKED 狀態(tài),簡(jiǎn)單來說 WAITING 和我們的 wait 方法有關(guān),而 BLOCKED 則和鎖有關(guān)。