jvm之性能監(jiān)控工具

jps 虛擬機進程狀況工具

jps [ options ] [ hostid ]
image.png

jstat:虛擬機統計信息監(jiān)視工具

jstat(JVM Statistics Monitoring Tool)是用于監(jiān)視虛擬機各種運行狀態(tài)信息的命令行工具。它可以顯示本地或者遠程虛擬機進程中的類加載、內存、垃圾收集、即時編譯等運行時數據,在沒有GUI圖形界面、只提供了純文本控制臺環(huán)境的服務器上,它將是運行期定位虛擬機性能問題的常用工具。

jstat命令格式為:

jstat [ option vmid [interval[s|ms] [count]] ]

參數interval和count代表查詢間隔和次數,如果省略這2個參數,說明只查詢一次。假設需要每250毫秒查詢一次進程2764垃圾收集狀況,一共查詢20次,那命令應當是:

jstat -gc 2764 250 20
image.png
jstat -gcutil 2764
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.00 6.20 41.42 47.20 16 0.105 3 0.472 0.577

查詢結果表明:這臺服務器的新生代Eden區(qū)(E,表示Eden)使用了6.2%的空間,2個Survivor區(qū)(S0、S1,表示Survivor0、Survivor1)里面都是空的,老年代(O,表示Old)和永久代(P,表示Permanent)則分別使用了41.42%和47.20%的空間。程序運行以來共發(fā)生Minor GC(YGC,表示YoungGC)16次,總耗時0.105秒;發(fā)生Full GC(FGC,表示Full GC)3次,總耗時(FGCT,表示Full GCTime)為0.472秒;所有GC總耗時(GCT,表示GC Time)為0.577秒。

使用jstat工具在純文本狀態(tài)下監(jiān)視虛擬機狀態(tài)的變化,在用戶體驗上也許不如后文將會提到的JMC、VisualVM等可視化的監(jiān)視工具直接以圖表展現那樣直觀,但在實際生產環(huán)境中不一定可以使用圖形界面,而且多數服務器管理員也都已經習慣了在文本控制臺工作,直接在控制臺中使用jstat命令依然是一種常用的監(jiān)控方式。

jmap:Java內存映像工具

jmap(Memory Map for Java)命令用于生成堆轉儲快照(一般稱為heapdump或dump文件)。如果不使用jmap命令,要想獲取Java堆轉儲快照也還有一些比較“暴力”的手段譬如XX:+HeapDumpOnOutOfMemoryError參數,可以讓虛擬機在內存溢出異常出現之后自動生成堆轉儲快照文件,通過-XX:+HeapDumpOnCtrlBreak參數則可以使用[Ctrl]+[Break]鍵讓虛擬機生成堆轉儲快
照文件,又或者在Linux系統下通過Kill-3命令發(fā)送進程退出信號“恐嚇”一下虛擬機,也能順利拿到堆轉儲快照。

jmap的作用并不僅僅是為了獲取堆轉儲快照,它還可以查詢finalize執(zhí)行隊列、Java堆和方法區(qū)的詳細信息,如空間使用率、當前用的是哪種收集器等。

image.png
jmap -dump:format=b,file=eclipse.bin 3500
Dumping heap to C:\Users\IcyFenix\eclipse.bin ...
Heap dump file created

jstack:Java堆棧跟蹤工具

jstack(Stack Trace for Java)命令用于生成虛擬機當前時刻的線程快照(一般稱為threaddump或者javacore文件)。線程快照就是當前虛擬機內每一條線程正在執(zhí)行的方法堆棧的集合,生成線程快照的目的通常是定位線程出現長時間停頓的原因,如線程間死鎖、死循環(huán)、請求外部資源導致的長時間掛起等,都是導致線程長時間停頓的常見原因。線程出現停頓時通過jstack來查看各個線程的調用堆棧,就可以獲知沒有響應的線程到底在后臺做些什么事情,或者等待著什么資源。

image.png

可視化故障處理工具

JDK中除了附帶大量的命令行工具外,還提供了幾個功能集成度更高的可視化工具,用戶可以使用這些可視化工具以更加便捷的方式進行進程故障診斷和調試工作。這類工具主要包括JConsole、JHSDB、VisualVM和JMC四個。其中,JConsole是最古老,早在JDK 5時期就已經存在的虛擬機監(jiān)控工具,而JHSDB雖然名義上是JDK 9中才正式提供,但之前已經以sa-jdi.jar包里面的HSDB(可視化工具)和CLHSDB(命令行工具)的形式存在了很長一段時間。它們兩個都是JDK的正式成員,隨著JDK一同發(fā)布,無須獨立下載,使用也是完全免費的。

JHSDB:基于服務性代理的調試工具

JDK中提供了JCMD和JHSDB兩個集成式的多功能工具箱,它們不僅整合了上一節(jié)介紹到的所有基礎工具所能提供的專項功能,而且由于有著“后發(fā)優(yōu)勢”,能夠做得往往比之前的老工具們更好、更強大,表4-15所示是JCMD、JHSDB與原基礎工具實現相同功能的簡要對比。


image.png

本次,我們要借助JHSDB來分析一下代碼清單4-6中的代碼,并通過實驗來回答一個簡單問題:staticObj、instanceObj、localObj這三個變量本身(而不是它們所指向的對象)存放在哪里?

/**
* staticObj、instanceObj、localObj存放在哪里?
*/
public class JHSDB_TestCase {
static class Test {
static ObjectHolder staticObj = new ObjectHolder();
ObjectHolder instanceObj = new ObjectHolder();
void foo() {
ObjectHolder localObj = new ObjectHolder();
System.out.println("done"); // 這里設一個斷點
}
}
private static class ObjectHolder {}
public static void main(String[] args) {
Test test = new JHSDB_TestCase.Test();
test.foo();
}
}

答案讀者當然都知道:staticObj隨著Test的類型信息存放在方法區(qū),instanceObj隨著Test的對象實例存放在Java堆,localObject則是存放在foo()方法棧幀的局部變量表中。這個答案是通過前兩章學習的理論知識得出的,現在要做的是通過JHSDB來實踐驗證這一點。

image.png
image.png

請讀者注意一下圖中各個區(qū)域的內存地址范圍,后面還要用到它們。打開Windows->Console窗口,使用scanoops命令在Java堆的新生代(從Eden起始地址到To Survivor結束地址)范圍內查找
ObjectHolder的實例,結果如下所示:


image.png

果然找出了三個實例的地址,而且它們的地址都落到了Eden的范圍之內,算是順帶驗證了一般情況下新對象在Eden中創(chuàng)建的分配規(guī)則。再使用Tools->Inspector功能確認一下這三個地址中存放的對象,結果如圖


image.png

Inspector為我們展示了對象頭和指向對象元數據的指針,里面包括了Java類型的名字、繼承關系、實現接口關系,字段信息、方法信息、運行時常量池的指針、內嵌的虛方法表(vtable)以及接口方法表(itable)等。由于我們的確沒有在ObjectHolder上定義過任何字段,所以圖中并沒有看到任何實例字段數據

接下來要根據堆中對象實例地址找出引用它們的指針,JHSDB的Tools菜單中有ComputeReverse Ptrs來完成這個功能,果然找到了一個引用該對象的地方,是在一個java.lang.Class的實例里,并且給出了這個實例的地址,通過Inspector查看該對象實例,可以清楚看到這確實是一個java.lang.Class類型的對象實例,里面有一個名為staticObj的實例字段.

從《Java虛擬機規(guī)范》所定義的概念模型來看,所有Class相關的信息都應該存放在方法區(qū)之中,但方法區(qū)該如何實現,《Java虛擬機規(guī)范》并未做出規(guī)定,這就成了一件允許不同虛擬機自己靈活把握的事情。JDK 7及其以后版本的HotSpot虛擬機選擇把靜態(tài)變量與類型在Java語言一端的映射Class對象存放在一起,存儲于Java堆之中,從我們的實驗中也明確驗證了這一點。

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

JConsole(Java Monitoring and Management Console)是一款基于JMX(Java Manage-mentExtensions)的可視化監(jiān)視、管理工具。它的主要功能是通過JMX的MBean(Managed Bean)對系統進行信息收集和參數動態(tài)調整。JMX是一種開放性的技術,不僅可以用在虛擬機本身的管理上,還可以運行于虛擬機之上的軟件中,典型的如中間件大多也基于JMX來實現管理與監(jiān)控。虛擬機對JMX
MBean的訪問也是完全開放的,可以使用代碼調用API、支持JMX協議的管理控制臺,或者其他符合JMX規(guī)范的軟件進行訪問。

“概述”頁簽里顯示的是整個虛擬機主要運行數據的概覽信息,包括“堆內存使用情況”“線程”“類”“CPU使用情況”四項信息的曲線圖,這些曲線圖是后面“內存”“線程”“類”頁簽的信息匯總,

“內存”頁簽的作用相當于可視化的jstat命令,用于監(jiān)視被收集器管理的虛擬機內存(被收集器直接管理的Java堆和被間接管理的方法區(qū))的變化趨勢。我們通過運行代碼清單4-7中的代碼來體驗一下它的監(jiān)視功能。運行時設置的虛擬機參數為:

/**
* 內存占位符對象,一個OOMObject大約占64KB
*/
static class OOMObject {
public byte[] placeholder = new byte[64 * 1024];
}
public static void fillHeap(int num) throws InterruptedException {
List<OOMObject> list = new ArrayList<OOMObject>();
for (int i = 0; i < num; i++) {
// 稍作延時,令監(jiān)視曲線的變化更加明顯
Thread.sleep(50);
list.add(new OOMObject());
}
System.gc();
}
public static void main(String[] args) throws Exception {
fillHeap(1000);
}

這段代碼的作用是以64KB/50ms的速度向Java堆中填充數據,一共填充1000次,使用JConsole的“內存”頁簽進行監(jiān)視,觀察曲線和柱狀指示圖的變化。

程序運行后,在“內存”頁簽中可以看到內存池Eden區(qū)的運行趨勢呈現折線狀,如圖4-12所示。監(jiān)視范圍擴大至整個堆后,會發(fā)現曲線是一直平滑向上增長的。從柱狀圖可以看到,在1000次循環(huán)執(zhí)行結束,運行了System.gc()后,雖然整個新生代Eden和Survivor區(qū)都基本被清空了,但是代表老年代的柱狀圖仍然保持峰值狀態(tài),說明被填充進堆中的數據在System.gc()方法執(zhí)行之后仍然存活


image.png

image.png

提兩個小問題供讀者思考一下:
1)虛擬機啟動參數只限制了Java堆為100MB,但沒有明確使用-Xmn參數指定新生代大小,讀者能否從監(jiān)控圖中估算出新生代的容量?
2)為何執(zhí)行了System.gc()之后,圖4-12中代表老年代的柱狀圖仍然顯示峰值狀態(tài),代碼需要如何調整才能讓System.gc()回收掉填充到堆中的對象?

問題1答案:上圖顯示Eden空間為1,128,448 KB,因為沒有設置-XX:SurvivorRadio參數,所以Eden與Survivor空間比例的默認值為8∶1,因此整個新生代空間大約為1,128,448 KB×125%。
問題2答案:執(zhí)行System.gc()之后,空間未能回收是因為List<OOMObject>list對象仍然存活,fillHeap()方法仍然沒有退出,因此list對象在System.gc()執(zhí)行時仍然處于作用域之內。如果把System.gc()移動到fillHeap()方法外調用就可以回收掉全部內存。

線程監(jiān)控

如果說JConsole的“內存”頁簽相當于可視化的jstat命令的話,那“線程”頁簽的功能就相當于可視化的jstack命令了,遇到線程停頓的時候可以使用這個頁簽的功能進行分析。前面講解jstack命令時提到線程長時間停頓的主要原因有等待外部資源(數據庫連接、網絡資源、設備資源等)、死循環(huán)、鎖等待等,代碼清單4-8將分別演示這幾種情況。
代碼清單4-8 線程等待演示代碼

package jvm.tools;

import java.io.BufferedReader;
import java.io.InputStreamReader;

/**
 * @Description:
 * @Created on 2020-04-06
 */
public class JconsoleTest {
    /**
     * 線程死循環(huán)演示
     */
    public static void createBusyThread() {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) // 第41行
                    ;
            }
        }, "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 Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        br.readLine();
        createBusyThread();
        br.readLine();
        Object obj = new Object();
        createLockThread(obj);
    }
}

程序運行后,首先在“線程”頁簽中選擇main線程,如圖4-13所示。堆棧追蹤顯示BufferedReader的readBytes()方法正在等待System.in的鍵盤輸入,這時候線程為Runnable狀態(tài),Runnable狀態(tài)的線程仍會被分配運行時間,但readBytes()方法檢查到流沒有更新就會立刻歸還執(zhí)行令牌給操作系統,這種等待只消耗很小的處理器資源。

程序運行后,首先在“線程”頁簽中選擇main線程,如圖4-13所示。堆棧追蹤顯示BufferedReader的readBytes()方法正在等待System.in的鍵盤輸入,這時候線程為Runnable狀態(tài),Runnable狀態(tài)的線程仍會被分配運行時間,但readBytes()方法檢查到流沒有更新就會立刻歸還執(zhí)行令牌給操作系統,這種等待只消耗很小的處理器資源。

接著監(jiān)控testBusyThread線程,如圖4-14所示。testBusyThread線程一直在執(zhí)行空循環(huán),從堆棧追蹤中看到一直在MonitoringTest.java代碼的41行停留,41行的代碼為while(true)。這時候線程為Runnable狀態(tài),而且沒有歸還線程執(zhí)行令牌的動作,所以會在空循環(huán)耗盡操作系統分配給它的執(zhí)行時間,直到線程切換為止,這種等待會消耗大量的處理器資源。

image.png

testLockThread線程正處于正常的活鎖等待中,只要lock對象的notify()或notifyAll()方法被調用,這個線程便能激活繼續(xù)執(zhí)行。代碼清單4-9演示了一個無法再被激活的死鎖等待。

package jvm.tools;

/**
* 線程死鎖等待演示
*/
public 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)都是可省略的,兩個線程也可能會導致死鎖,不過那樣概率太小,需要嘗試運行很多次才能看到死鎖的效果。如果運氣不是特別差的話,上面帶for循環(huán)的版本最多運行兩三次就會遇到線程死鎖,程序無法結束。造成死鎖的根本原因是Integer.valueOf()方法出于減少對象創(chuàng)建次數和節(jié)省內存的考慮,會對數值為-128~127之間的
Integer對象進行緩存[2],如果valueOf()方法傳入的參數在這個范圍之內,就直接返回緩存中的對象。也就是說代碼中盡管調用了200次Integer.valueOf()方法,但一共只返回了兩個不同的Integer對象。假如某個線程的兩個synchronized塊之間發(fā)生了一次線程切換,那就會出現線程A在等待被線程B持有的Integer.valueOf(1),線程B又在等待被線程A持有的Integer.valueOf(2),結果大家都跑不下去的情況.

出現線程死鎖之后,點擊JConsole線程面板的“檢測到死鎖”按鈕,將出現一個新的“死鎖”頁簽,

image.png
image.png
image.png

JIT生成代碼反匯編

在《Java虛擬機規(guī)范》里詳細定義了虛擬機指令集中每條指令的語義,尤其是執(zhí)行過程前后對操作數棧、局部變量表的影響。這些細節(jié)描述與早期Java虛擬機(Sun Classic虛擬機)高度吻合,但隨著技術的發(fā)展,高性能虛擬機真正的細節(jié)實現方式已經漸漸與《Java虛擬機規(guī)范》所描述的內容產生越來越大的偏差,《Java虛擬機規(guī)范》中的規(guī)定逐漸成為Java虛擬機實現的“概念模型”,即實現只保證與規(guī)范描述等效,而不一定是按照規(guī)范描述去執(zhí)行。由于這個原因,我們在討論程序的執(zhí)行語義問題(虛擬機做了什么)時,在字節(jié)碼層面上分析完全可行,但討論程序的執(zhí)行行為問題(虛擬機是怎樣做的、性能如何)時,在字節(jié)碼層面上分析就沒有什么意義了,必須通過其他途徑解決。

至于分析程序如何執(zhí)行,使用軟件調試工具(GDB、Windbg等)來進行斷點調試是一種常見的方式,但是這樣的調試方式在Java虛擬機中也遇到了很大麻煩,因為大量執(zhí)行代碼是通過即時編譯器動態(tài)生成到代碼緩存中的,并沒有特別簡單的手段來處理這種混合模式的調試,不得不通過一些曲線的間接方法來解決問題。在這樣的背景下,本節(jié)的主角——HSDIS插件就正式登場了。

HSDIS是一個被官方推薦的HotSpot虛擬機即時編譯代碼的反匯編插件,它包含在HotSpot虛擬機的源碼當中,在OpenJDK的網站[3]也可以找到單獨的源碼下載,但并沒有提供編譯后的程序。HSDIS插件的作用是讓HotSpot的-XX:+PrintAssembly指令調用它來把即時編譯器動態(tài)生成的本地代碼還原為匯編代碼輸出,同時還會自動產生大量非常有價值的注釋,這樣我們就可以通過輸出的
匯編代碼來從最本質的角度分析問題。讀者可以根據自己的操作系統和處理器型號,從網上直接搜索、下載編譯好的插件,直接放到JDK_HOME/jre/bin/server目錄(JDK 9以下)或JDK_HOME/lib/amd64/server(JDK 9或以上)中即可使用。如果讀者確實沒有找到所采用操作系統的對應編譯成品,那就自己用源碼編譯一遍

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容