JVM 第四篇:視覺化 JVM 故障處理工具

極客挖掘機發表於2020-10-12

本文內容過於硬核,建議有 Java 相關經驗人士閱讀。

1. 視覺化工具

在 JDK 中為我們提供了大量的 JVM 故障處理工具,都在 JDK 的 bin 目錄下:

這其中除了大量的命令列工具以外,還為我們提供了更加方便快捷的視覺化工具,主要是以下這 4 個:

  • JConsole: 最古老的工具,早在 JDK 5 時期就已經存在的虛擬機器監控工具。
  • JHSDB: 名義上在 JDK 9 中才正式提供,但之前已經以 sa-jdi.jar 包裡面的 HSDB(視覺化工具) 和 CLHSDB(命令列工具) 的形式存在了很長一段時間。
  • VisualVM: 在 JDK 6 Update 7 中首次釋出,直到 JRockit Mission Control 與 OracleJDK 的融合工作完成之前,它都曾是 Oracle 主力推動的多合一故障處理工具,現在它已經從 OracleJDK 中分離出來,成為一個獨立發展的開源專案。
  • JMC: Java Mission Control ,曾經是大名鼎鼎的來自 BEA 公司的圖形化診斷工具,隨著 BEA 公司被 Oracle 收購,它便被融合進 OracleJDK 之中。在 JDK 7 Update 40 時開始隨 JDK 一起釋出,後來 Java SE Advanced 產品線建立, Oracle 明確區分了 Oracle OpenJDK 和 OracleJDK 的差別, JMC 從 JDK 11 開始又被移除出 JDK 。

2. HSDB

HSDB(Hotspot Debugger) 是 JDK 自帶的工具,用於檢視 JVM 執行時的狀態。

使用方式由於在 JDK 9 之前沒有正式提供,所以也未在 JDK 的 bin 目錄下提供直接可執行檔案,需要在命令列執行命令才能啟動。

首先在命令列中先 cd 至 C:\Program Files\Java\jdk1.8.0_221\lib 目錄,然後執行:

java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB

我在執行這句命令的時候報了個錯:

Exception in thread "Thread-1" java.lang.UnsatisfiedLinkError: Can't load library: C:\Program Files\Java\jdk-11.0.4\bin\sawindbg.dll
        at java.base/java.lang.ClassLoader.loadLibrary(ClassLoader.java:2620)
        at java.base/java.lang.Runtime.load0(Runtime.java:767)
        at java.base/java.lang.System.load(System.java:1831)
        at sun.jvm.hotspot.debugger.windbg.WindbgDebuggerLocal.<clinit>(WindbgDebuggerLocal.java:661)
        at sun.jvm.hotspot.HotSpotAgent.setupDebuggerWin32(HotSpotAgent.java:567)
        at sun.jvm.hotspot.HotSpotAgent.setupDebugger(HotSpotAgent.java:335)
        at sun.jvm.hotspot.HotSpotAgent.go(HotSpotAgent.java:304)
        at sun.jvm.hotspot.HotSpotAgent.attach(HotSpotAgent.java:140)
        at sun.jvm.hotspot.HSDB.attach(HSDB.java:1184)
        at sun.jvm.hotspot.HSDB.access$1700(HSDB.java:53)
        at sun.jvm.hotspot.HSDB$25$1.run(HSDB.java:456)
        at sun.jvm.hotspot.utilities.WorkerThread$MainLoop.run(WorkerThread.java:66)
        at java.base/java.lang.Thread.run(Thread.java:834)

含義是有一個 sawindbg.dll 在 jdk 的目錄下找不到,因為我本地有多個 jdk ,配置環境變數的是 jdk 11 ,我在 jdk 8 的 jre 的 bin 目錄下找到了這個檔案,直接 copy 到 jdk 11 的bin 目錄下解決此問題。

執行完成後會開啟這麼個介面:

接下來,我們寫一小段測試程式碼:

public abstract class A {
    public void printMe() {
        System.out.println("I am A class");
    }
    public abstract void sayHello();
}

public class B extends A {
    @Override
    public void sayHello() {
        System.out.println("I am B class");
    }
}

public class HSDB_Test {
    public static void main(String[] args) throws IOException {
        A obj = new B();
        // 無意義,單純用作卡住主執行緒,防止執行緒結束
        System.in.read();
        System.out.println(obj);
    }
}

首先在命令列中使用命令 jps -l 檢視 Java 程式的 pid :

PS C:\Users\inwsy> jps -l
1648 com.geekdigging.lesson04.jvmtools.HSDB_Test
8704
9280 org.jetbrains.jps.cmdline.Launcher
14724 jdk.jcmd/sun.tools.jps.Jps
3220 org/netbeans/Main
15144
20572 org.jetbrains.jps.cmdline.Launcher
7548 sun.jvm.hotspot.HSDB

可以看到,我們剛寫的測試方法的 pid 是 1648 ,在 HSDB 中點選 File > Attach to Hotspot process :

第一個看到的就是當前程式中的執行緒:

到這裡,我們的準備工作就已經結束,接著我們使用 Tools > Class Browser 找到物件 B 的記憶體地址:

圖中紅框框起來的是我自己寫的三個類,可以看到我這裡 B 的記憶體地址是 0x00000007c0060c18

接下來,使用 Tools > Inspector 檢視這個物件的詳細資訊:

vtable 是虛表方法,這裡我們看到 class B 有 7 個虛表方法,因為所有的物件都繼承自 Object ,所以 B 繼承了 Object 的 5 個方法,然後還繼承了 A 的一個方法,自己重寫了一個方法,總共是 7 個方法。

這個我們可以進行一下驗證,可以在 Windows > Console 中使用 mem 命令進行檢視。

那麼我們可以開始計算, vtable 是在 instanceKlass 物件例項的尾部,而 instanceKlass 大小在 64 位系統的大小為 0x1B8 ,因此 vtable 的起始地址等於 instanceKlass 的記憶體首地址加上 0x1B8 等於 7C0060DD0 。

接下來,我們在 Windows > Console 中使用 mem 命令進行驗證:

第一列是方法實際在堆中的記憶體地址,第二列則是記憶體指標地址,我們可以將拿到的記憶體指標地址去 A , B 和 Object 中分別檢視,可以看到前 5 行對應的是 Object 的方法,第 6 行對應的是 A 物件中的方法,第 7 行則對應 B 物件中的方法。

3. JConsole

JConsole(Java Monitoring and Management Console) 是一款基於 JMX(Java Manage-ment Extensions) 的視覺化監視、管理工具。它的主要功能是通過 JMX 的 MBean(Managed Bean) 對系統進行資訊收集和引數動態調整。

JConsole 位於 JDK/bin 這個目錄下,直接雙擊 jconsole.exe 就可以直接啟動,在啟動之後,會自動搜尋出當前在本機執行的所有虛擬機器程式。

這裡可以看到我本機目前執行了一個 JConsole ,一個 idea ,還有一個啟動的 tomcat 的原始碼。

隨便雙擊一個服務,進入主頁面:

可以看到主介面裡共包括概述、記憶體、執行緒、類、 VM 摘要、 MBean 六個頁籤。

還是來個小示例,我們來了解下它的監控功能。

public class MonitoringTest {
    // 記憶體佔位物件,一個物件大約 64KB
    static class OOMObject {
        public byte[] placeholder = new byte[64 * 1024];
    }

    public static void fillHeap(int nums) throws InterruptedException {
        List<OOMObject> list = new ArrayList<>();
        for (int i = 0; i < nums; i++) {
            Thread.sleep(50);
            list.add(new OOMObject());
        }
        System.gc();
    }

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

這個案例是使用大約 64KB/50ms 的速度向 Java 堆中填充資料,一共填充 1000 次。

程式執行後可以看到,在整個 Java 堆中,曲線一直是平滑向上的。

切換到記憶體標籤頁,檢視 Eden 後可以發現,整個 Eden 的圖形是一個折線:

再切換到 Gen ,可以看到整個老年代也是摺疊向上的:

我們已經在程式碼里加了 System.gc() ,為什麼看起來沒生效呢?

因為 System.gc() 是在 fillHeap() 方法中的,在 GC 的時候,還在作用域中,想要正常回收老年代,需要將 System.gc() 這段程式碼轉移到 fillHeap() 外面。

先修改下程式碼:

public static void main(String[] args) throws InterruptedException {
    fillHeap(1000);
    System.gc();
    // GC 後停頓 3s ,方便觀察影像
    Thread.sleep(3000);
}

可以看到在最後程式結束的時候, Gen 的柱狀圖已經沒有記憶體佔用了,記憶體回收成功。

3. VisualVM

VisualVM(All-in-One Java Troubleshooting Tool)是功能最強大的執行監視和故障處理程式之一,曾經在很長一段時間內是 Oracle 官方主力發展的虛擬機器故障處理工具。

VisualVM 同樣在 JDK/bin 這個目錄下,雙擊 jvisualvm.exe 即可執行。在啟動之後,直接在左側會顯示當前在本機執行的所有虛擬機器程式。

VisualVM 基於 NetBeans 平臺開發工具,所以一開始它就具備了通過外掛擴充套件功能的能力,有了外掛擴充套件支援, VisualVM 可以做到:

  • 顯示虛擬機器程式以及程式的配置、環境資訊(jps、jinfo)。
  • 監視應用程式的處理器、垃圾收集、堆、方法區以及執行緒的資訊(jstat、jstack)。
  • dump 以及分析堆轉儲快照(jmap、jhat)。
  • 方法級的程式執行效能分析,找出被呼叫最多、執行時間最長的方法。
  • 離執行緒序快照:收集程式的執行時配置、執行緒 dump 、記憶體 dump 等資訊建立一個快照,可以將快照傳送開發者處進行 Bug 反饋。
  • 其他外掛帶來的無限可能性。

VisualVM 的外掛可以在 工具->外掛 中聯網後直接安裝。

我這裡只安裝了兩個最常用的,一個是 GC 監控的外掛,還有一個可以動態插入除錯程式的外掛。

我這裡使用最常用的開發工具 IDEA 啟動過程演示一下通過 VisualVM 監控程式 GC 。

首先我們啟動 IDEA ,直到 IDEA 可以正常操作,看下 VisualVM 的 GC 監控。

在主資訊皮膚,可以看到 IDEA 所使用 JVM 的版本資訊,可以看到具體的 JAVA_HOME 路徑,還可以看到具體的 JVM 引數,這裡可以看到 IDEA 啟動時設定的預設最小堆和最大堆記憶體的設定分別是 128MB 和 750 MB ,所使用的垃圾回收器則是 CMS 收集器。

然後點選 Visual GC,可以看到:

在啟動過程中, Class 載入消耗了 28s 左右,而 Class 編譯則消耗了 35s 。並且在這個過程中, Minor GC 被觸發了 149 次,消耗只有 713ms ,我們更加關注的 Full GC 更是一次都沒有觸發,消耗為 0 。

因為 IDEA 預設使用的是 CMS 收集器,如果我們換成 G1 收集器會不會更快一些呢?

首先,找到 IDEA 的配置檔案,我的 IDEA 是通過 Toolbox 進行安裝的,所以我的 IDEA 的配置檔案的路徑有點奇怪 D:\Program Files\JetBrains\apps\IDEA-U\ch-0\202.7660.26.vmoptions

先把這個檔案備份到桌面一個,防止改壞了導致 IDEA 不能使用。

刪掉現有的垃圾回收器配置 -XX:+UseConcMarkSweepGC ,增加 G1 收集器的配置:

-XX:+UseG1GC

其餘的配置不做修改,直接關閉 IDEA 重啟,再看下 GC 情況。

首先先看下主皮膚,看下我們的 GC 收集器是否已經切換成功:

然後再看下 GC 皮膚:

Minor GC 竟然被觸發了 271 次,而且消耗達到了 853ms ,好吧,看來在客戶端還是更適合使用 CMS 做為垃圾回收器。

我們再修改下 -Xmx 這個配置,將配置的大小縮減為現在的一半,再把 GC 換回原有的 CMS ,看下 Full GC 的情況:

可以看到, Full GC 整整發生了 46 次,並且耗時超過了 21s ,而且這是 IDEA 的介面上也開始彈出警告,警告我們記憶體不足了,需要調整。

嚇得我趕緊改回了原有配置,順便把 -Xmx 的大小加到了 1024 ,儘量減少 Full GC 的情況。

4. Java Mission Control

JMC 同樣在 JDK/bin 這個目錄下,雙擊 jmc.exe 即可執行。

開啟後在 JVM 瀏覽器皮膚中有兩個選項,一個是 MBean ,一個是 JFR 飛行記錄器。

關於 MBean 這部分資料,與 JConsole 和 VisualVM 上取到的內容是一樣的,只是展示形式上有些差別,就不多說了。

雙擊「飛行記錄器」,將會出現「飛行記錄器」視窗(如果第一次使用,還會收到解鎖商業功能的警告窗)。

注意:在使用前需要在 JVM 中增加如下兩個引數,含義是解鎖 JFR 功能的鎖定。

-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder

飛行記錄報告裡包含以下幾類資訊:

  • 一般資訊:關於虛擬機器、作業系統和記錄的一般資訊。
  • 記憶體:關於記憶體管理和垃圾收集的資訊。
  • 程式碼:關於方法、異常錯誤、編譯和類載入的資訊。
  • 執行緒:關於應用程式中執行緒和鎖的資訊。
  • I/O:關於檔案和套接字輸入、輸出的資訊。
  • 系統:關於正在執行Java虛擬機器的系統、程式和環境變數的資訊。
  • 事件:關於記錄中的事件型別的資訊,可以根據執行緒或堆疊跟蹤,按照日誌或圖形的格式檢視。

5. 小結

這 4 款視覺化工具看下來,個人感覺還是最後一個 JMC 對使用者來講最友好, MBean 資料來源展示了大量的當前 JVM 的資訊,而且全都以圖表的形式進行了展現,更加給力還是它的 JFR 功能,可以記錄一段時間內所有的操作,並且以圖表的形式進行展現,對我們分析問題時候的幫助無疑是巨大的。

當然,喜歡用哪款工具完全是個人喜好,比如 VisualVM 也很強大,可能它本身的功能沒那麼強,但是它可以安裝外掛,完全根據需要進行外掛的安裝,這個玩法非常 DIY ,總的算下來,我還是喜歡使用 VisualVM 更多一些。

參考

《深入理解Java虛擬機器:JVM高階特性與最佳實踐_周志明》

https://www.cnblogs.com/alinainai/p/11070923.html

相關文章