7、虛擬機性能監(jiān)控與故障處理工具(2)(JVM筆記)

二、JDK的可視化工具

2.1 JConsole:Java監(jiān)視與管理控制臺

JConsole(Java Monitoring and Management Console)是一種基于JMX的可視化監(jiān)視、管理工具。它管理部分的功能是針對JMX MBean進行管理,由于MBean可以使用代碼、中間件服務器的管理控制臺或者所有符合JMX規(guī)范的軟件進行訪問。

2.1.1 啟動JConsole

JDKbin目錄中可以直接雙擊運行此工具。

1

啟動后我們可以對其中一個進程進行監(jiān)控(這里我們只是啟動了JConsole進程),得到的監(jiān)控界面如下:
2

說明:“概述”頁面顯示的是整個虛擬機主要運行數(shù)據(jù)的概覽,其中包括“對內(nèi)存使用情況”、“線程”、“類”、“CPU使用情況”四種信息的曲線圖。

2.1.2 內(nèi)存監(jiān)控

“內(nèi)存”頁相當于可視化的jstat命令,用于監(jiān)視受收集器管理的虛擬機內(nèi)存(Java堆和永久代)的變化趨勢。這里通過例子說明(這里親自試驗了書中的例子,但是拿到的曲線和書中有點不一樣,這里還是以書中為準):

代碼如下:

//使用java -Xms100m -Xmx100m -XX:+UseSerialGC運行
public static void main(String[] args) throws Exception{
    fileHeap(1000);
}

static class OOMObject{
    public byte[] placeholder = new byte[64 * 1024];

}

public static void fileHeap(int num) throws InterruptedException{

    List<OOMObject> list = new ArrayList<OOMObject>();
    for(int i = 0; i < num; i++){
        Thread.sleep(50);
        list.add(new OOMObject());
    }
    System.gc();
}

3

說明:這段代碼的作用是以64KB/50ms的速度往Java堆中填充數(shù)據(jù),一共填充1000次,使用JConsole的“內(nèi)存”頁進行監(jiān)視,曲線變化如上圖。這里可以看到,內(nèi)存池Eden區(qū)的運行趨勢呈現(xiàn)折線狀。監(jiān)視范圍擴大至整個堆后,會發(fā)現(xiàn)曲線是一條向上增長的平滑曲線。之所以呈現(xiàn)折線是因為當Eden區(qū)被填滿時進行一次GC。從柱狀圖中可以看到,在1000次循環(huán)執(zhí)行結(jié)束,運行了Sytem.gc()后,雖然整個新生代EdenSurvivor區(qū)都基本被清空了,但是代表老年代的柱狀圖仍然保持峰值狀態(tài),說明被填充進堆中的數(shù)據(jù)在System.gc()方法執(zhí)行后仍然存活。

  • 問題一:虛擬機啟動參數(shù)只限制了Java堆為100MB,沒有指定-Xmn參數(shù)(-Xms初始堆大小,-Xmx最大堆大?。?,能否從監(jiān)控圖中估計出新生代有多大?
    從圖中可以看到Eden空間為27328KB,因為沒有設置-XX:SurvivorRadio參數(shù),所以EdenSurvivor空間比例默認為8:1,整個新生代空間大約為27328KB*125%=34160KB。

  • 問題二、為何執(zhí)行了System.gc()之后,圖中代表的老年代的柱狀圖仍然顯示峰值狀態(tài),代碼需要如何改動才能讓System.gc()回收掉填充到堆中的對象?
    執(zhí)行完System.gc()后,空間未能回收是因為在List<OOMObject> list對象仍然存活,fileHeap()方法仍然沒有退出,因此list對象在System.gc()執(zhí)行時仍然處于作用域之內(nèi)。如果把System.gc()移動到fileHeap()方法外調(diào)用就可以回收掉全部內(nèi)存。

2.1.3 線程監(jiān)控

如果上面的“內(nèi)存”頁簽相當于可視化的jstat命令的話,“線程”頁簽的功能相當于可視化的jstack命令,遇到線程停頓時可以使用這個頁簽進行監(jiān)控分析。之前說過線程長時間停頓的主要原因主要有:等待外部資源(數(shù)據(jù)庫連接、網(wǎng)絡資源、設備資源等)、死循環(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){
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    br.readLine();
    createBusyThread();
    br.readLine();
    Object obj = new Object();
    createLockThread(obj);
}

說明:程序運行后,首先在“線程”頁簽中選擇main線程,如圖所示。堆棧追蹤顯示BufferedReaderreadBytes方法中等待System.in的鍵盤輸入,這時線程為Runnable狀態(tài),Runnable狀態(tài)的線程會被分配運行時間,但readBytes方法檢查到流沒有更新時會立刻歸還執(zhí)行令牌,這種等待只消耗很小的CPU資源。

4

接著監(jiān)控testBusyThread線程,如圖所示。此時處于一個死循環(huán)中,不會歸還線程執(zhí)行令牌,會消耗很多CPU資源。

5

在執(zhí)行testLockThread線程時,在等待著lock對象的nofifynotifyAll方法的出現(xiàn),此時線程處于WAITTING狀態(tài),在被喚醒之前是不會被分配執(zhí)行時間的。

6

這個線程只要lock對象的notify()notifyAll()方法被調(diào)用就會被激活,繼續(xù)執(zhí)行。下面的代碼演示了無法再被激活的死鎖等待。

//線程死鎖等待演示
static class SynAddRunalbe implements Runnable{
    int a , b;
    public SynAddRunalbe(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 SynAddRunalbe(1, 2)).start();
        new Thread(new SynAddRunalbe(2, 1)).start();
    }
}

說明:這段代碼開了200個線程去分別計算1+2以及2+1的值,一般的話,for循環(huán)只需要運行2~3次就會遇到線程死鎖,程序無法結(jié)束。造成死鎖的原因是Integer.valueOf()方法基于減少對象創(chuàng)建次數(shù)和節(jié)省內(nèi)存的考慮,[-128, 127]之間的數(shù)字會被緩存,當valueOf()方法傳入?yún)?shù)在這個范圍之內(nèi),將直接返回緩存中的對象。即代碼中調(diào)用了200valueOf()方法一共就只返回了兩個不同的對象。加入在某個線程的兩個synchronized塊之間發(fā)生了一次線程切換,那就會出現(xiàn)線程A等著被線程B持有的Integer.valueOf(1),線程B又等著被線程A持有的Integer.valueOf(2),結(jié)果出現(xiàn)大家都拋不下去的情景。

出現(xiàn)死鎖之后,點擊JConsole線程面板的“監(jiān)測到死鎖”的按鈕,將出現(xiàn)一個新的“死鎖”頁簽,如圖所示。

7

從圖中可以看到,線程Thread-43在等待一個被線程Thread-12持有的Integer對象,而點擊線程Thread-12則顯示它也在等待一個Integer對象,被線程Thread-43持有,這樣就發(fā)生了死鎖。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容