(七)虛擬機(jī)性能監(jiān)控與故障處理工具

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)查詢了。

JConsole連接頁(yè)面.png
JConsole主界面.png

如上圖所示,主界面共包括“概述”、“內(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);
    }
}
Eden區(qū)內(nèi)存變化狀況.png

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

堆內(nèi)存使用量.png

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資源。

main線程.png

接著監(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資源。

testBusyThread線程.png

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

testLockThread線程.png

最后,我們來(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)了死鎖等待。

線程死鎖.png
線程死鎖快照.png

3.2.多合一故障處理工具:VisualVM

3.3.內(nèi)存分析工具:MAT(Eclipse Memory Analyzer Tool)

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

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