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