JVM08-虛擬機故障處理之可視化故障處理工具JConsole工具

前言

上一篇我們介紹了JVM07-虛擬機故障處理命令行工具。這一篇將繼續介紹虛擬機故障處理之可視化故障處理工具JConsole工具。這個工具我們可以在JDK的bin目錄下找到。

JConsole的介紹

JConsole是一款基於JMX(Java Management Extensions)的可視化監視、管理工具。它主要是通過JMX的MBean對系統進行信息收集和參數動態調整。JMX是一種開放性的技術,不僅可以用在虛擬機本身的管理上,還可以運行於虛擬機之上的軟件中,典型的如中間件大多也是基於JMX來實現管理和監控的。

JConsole的使用

1. 啓動JConsole

運行JDK/bin目錄下的jconsole.exe就可以啓動JConsole。JConsole啓動之後會自動搜索出本機運行的所有虛擬機進程(只能監控運行在本虛擬機的進程),而不需要用戶自己使用jps來查詢,如圖,有如下進程,雙擊選中JConsoleTest進程其中一個進程便可以進入主界面開始監控JConsoleTest進程的相關信息。同時JMX支持跨服務器的管理。
在這裏插入圖片描述

內存監控

"內存"頁籤的作用相當於可視化的jstat命令,用於監控被收集器管理的虛擬機內存(被收集器直接管理Java堆和被間接管理的方法區)的變化趨勢。如下 JConsoleTest類循環創建OOMObject對象,每隔50ms創建一個,就相當於以 100KB/50ms的速度向Java堆中填充數據。一共填充1000次。我們可以進入內存 頁籤中觀察內存變化趨勢。
運行前的內存設置如下:設置堆內存最大爲100m。

-Xms100m -Xmx100m  -XX:+UseSerialGC

上面我們只是指定了整個堆的內存,沒有指定新生代的大小。那麼整個新生代的堆內存大小是多少呢?看下圖:
在這裏插入圖片描述

如上圖,我們看到Eden區域的內存一直在平穩的增加,直到執行System.gc();之後才下降下來。
看左下角可以知道Eden區域的大小是27,328 KB,同時沒有設置-XX:SurvivorRation,按照JVM默認的設置Eden與Survivor的比例爲8:1,而新生代有兩個Survivor區域。所以整個新生代的內存大小是27328KB*1.25=34160KB
同時我們注意到在循環填充完數據之後,執行System.gc();之後,新生代的Eden和Survivor區域已使用內存明顯下降,但是老年代的內存還處於高位,這是爲啥呢?這是因爲System.gc();是放在setOOMObject方法內部調用的,而在該方法內oomObjectList對象還是有效的,是不能被回收的。所以老年代還是處於高位。要是oomObjectList對象也能被回收,只需要將System.gc();的調用放到setOOMObject方法外部調用。這樣才能使垃圾收集器可以收集老年代中的oomObjectList對象。

public class JConsoleTest {
    public static void main(String[] args) throws InterruptedException {
        setOOMObject(1000);
    }

    /**
     * 內存佔位符對象,一個OOMObject大約佔100KB。
     */
    static class OOMObject{
        private static final byte[] param = new byte[100 * 1024];
    }

    public static void setOOMObject(int num) throws InterruptedException {
        List<OOMObject> oomObjectList = new ArrayList<>();
        Thread.sleep(3000);
        for (int i = 0; i < num; i++) {
            System.out.println("*********第["+i+"]次設值");
            //休息50毫秒
            Thread.sleep(50);
            oomObjectList.add(new OOMObject());
        }
        System.gc();
    }
}

線程監控

說完了內存監控,我們接着來看看線程監控,如果說JConsole的"內存"頁籤相當於可視化的jstat命令的話,那"線程"頁籤的功能就相當於可視化的jstack命令了,遇到線程停頓的時候可以使用這個頁籤的功能進行分析。我們知道線程長時間停頓的主要原因有等待外部資源(數據庫連接、網絡資源、設備資源等)、死循環、鎖等待等。下面用MonitoringTest類來模擬下等待外部資源、 死循環等待和鎖等待等情況。

public class MonitoringTest {
    /**
     * 線程死循環演示
     */
    public static void createBusyThread() {
        new Thread(() -> {
            while (true) {
            }
        }, "testBusyThread").start();
    }

    /**
     * 線程鎖等待演示
     * @param lock
     */
    public static void createLockThread(final Object lock) {
        new Thread(() -> {
            synchronized (lock) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "testLockThread").start();
    }

    public static void main(String[] args) throws IOException {
        //等待外部資源
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        bufferedReader.readLine();

        createBusyThread();
        bufferedReader.readLine();

        Object obj  = new Object();
        createLockThread(obj);
    }
}

運行MonitoringTest之後,在JConsole中觀察其運行情況,首先我們在"線程"頁籤中選中main線程、堆棧追蹤顯示BufferedReader的readBytes()方法正在等待System.in的鍵盤輸入。這時候線程爲Runnable狀態,Runnable狀態的線程仍會被分配運行時間,但readBytes()方法檢查到流沒有更新就會立即歸還令牌給操作系統,這種等待只消耗很小的處理器資源。如下圖所示:

在這裏插入圖片描述

接着監控testBusyThread線程,如下圖所示:testBusyThread線程一直在執行空循環,從堆棧追蹤可以看到在MonitoringTest代碼的第17行停留,第17行的代碼爲while(true)。這時候線程爲Runable狀態,而且沒有歸還線程執行令牌的動作,所以會空循環耗盡系統分配給它的執行時間,直到線程切換爲止,這種等待會消耗大量的處理器資源。

在這裏插入圖片描述
最後我們看看testLockThread線程在等待lock對象的notify()或者notifyAll()方法的出現,線程這時候處於WAITING狀態,在重新喚醒之前不會被分配執行時間。同時會釋放佔用的鎖對象。testLockThread線程正處於正常的活鎖等待中,只要lock對象的notify()或notifyAll()方法被調用,這個線程便能激活繼續執行。相關監控結果如下圖所示:

在這裏插入圖片描述
說完了活鎖的情況,下面我們來看一個死鎖的情況。如下JConsoleDeadLockTest類,在Runable的run方法中加了兩把鎖(synchronized),鎖對象分別是 Integer.valueOf(a)Integer.valueOf(b)。在main方法中定義兩個線程,傳入的a,b值相反。這種情況下就會出現死鎖,原因是Integer.valueOf()方法處於減少對象創建次數和節省內存的考慮,會對數值爲-128~127之間的Integer對象進行緩存,如果valueOf()方法傳入的參數在這個範圍內,就直接返回緩存中的對象。也就是說盡管調用了100次Integer.valueOf()方法,但一共只返回了兩個不同的Integer對象,假如某個線程在兩個synchronized塊之間發生了一次線程切換,那就會出現線程A在等待了線程B持有的Integer.valueOf(1),而線程B又在等待線程A持有的Integer.valueOf(2),結果就發生了死鎖。

public class JConsoleDeadLockTest {

    static class SyncAddRunner implements Runnable {
        int a, b;

        public SyncAddRunner(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 SyncAddRunner(1, 2),"線程一").start();
            new Thread(new SyncAddRunner(2, 1), "線程二").start();
        }
    }
}

我們接着看下在 JConsole中的監控情況。同樣的選中線程 頁籤,然後,點擊檢查死鎖 按鈕,就可以看到 線程一和線程二發生了死鎖。
在這裏插入圖片描述

總結

本文主要介紹了JConsole工具的使用場景,以及使用方法。JConsole是JDK自帶的可視化監控工具,在實際的工作中我們可以用它來分析系統的運行狀況。

參考

深入理解Java虛擬機(第3版)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章