天天看點

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

作者:一個即将被退役的碼農

注意: 篇幅較長,建議收藏後再仔細閱讀!!!!!!!!!!

目錄:

一.引言

二.基礎故障處理工具

2.1 概述

2.2. jps:虛拟機程序狀況工具

2.3. jstat:虛拟機統計資訊監視工具

2.3. jinfo:Java配置資訊工具

2.5. jmap:Java記憶體映像工具

2.7. jstack:Java堆棧跟蹤工具

2.8. 基礎工具總結

三. 可視化故障處理工具

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

3.2. JConsole:Java監視與管理控制台

3.3. JVisualVM:多合一故障處理工具

3.4. Java Mission Control(JMC):可持續線上的監控工具

四. HotSpot虛拟機插件及工具

一.引言

給一個系統定位問題的時候,知識、經驗是關鍵基礎,資料是依據,工具是運用知識處理資料的手段。

異常堆棧、虛拟機運作日志、垃圾收集器日志、線程快照(threaddump/javacore檔案)、堆轉儲快照(heapdump/hprof檔案)。

工具永遠都是知識技能的一層包裝,沒有什麼工具是“秘密武器”,擁有了就能“包治百病”。

二.基礎故障處理工具

2.1 概述

選擇采用Java語言本身來實作這些故障處理工具

當應用程式部署到生産環境後,無論是人工實體接觸到伺服器還是遠端Telnet到伺服器上都可能會受到限制。

借助這些工具類庫裡面的接口和實作代碼,開發者可以選擇直接在應用程式中提供功能強大的監控分析功能。

啟用 JMX 功能

JDK5或以下版本,在程式啟動時請添加參數“-Dcom.sun.management.jmxremote”開啟JMX管理功能。

JDK6或以上版本,預設開啟了JMX管理。

2.2. jps:虛拟機程序狀況工具

JDK的很多小工具的名字都參考了UNIX指令的命名方式,jps(JVM Process Status Tool)是其中的典型。除了名字像UNIX的ps指令之外,它的功能也和ps指令類似:可以列出正在運作的虛拟機程序,并顯示虛拟機執行主類(Main Class,main()函數所在的類)名稱以及這些程序的本地虛拟機唯一ID(LVMID,Local Virtual Machine Identifier)。雖然功能比較單一,但它絕對是使用頻率最高的JDK 指令行工具,因為其他的JDK工具大多需要輸入它查詢到的LVMID來确定要監控的是哪一個虛拟機程序。對于本地虛拟機程序來說,LVMID與作業系統的程序ID(PID,Process Identifier)是一緻的,使用Windows的任務管理器或者UNIX的ps指令也可以查詢到虛拟機程序的LVMID,但如果同時啟動了多個虛拟機程序,無法根據程序名稱定位時,那就必須依賴jps指令顯示主類的功能才能區分了。

jps指令格式:

jps [ options ] [ hostid ]

jps可以通過RMI協定開啟了RMI服務的遠端虛拟機程序狀态,hostid為RMI系統資料庫中注冊的主機名。

jps常用的option選項:

選項 作用
-q 隻輸出LVMID
-m 輸出虛拟機程序啟動時傳遞給主類main()函數的參數
-l 輸出主類全名,如果程序執行的事jar包,輸出jar路徑
-v 輸出虛拟機程序啟動時JVM參數

案例

public class Jstat {
    /**
     * vm參數為 -Xms30m -Xmx30m -Xmn10m
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(1000000);
    }
}           

運作之後,使用jps指令,将會展示虛拟機程序id和名字:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

2.3. jinfo:Java配置資訊工具

jstat是用于監視虛拟機各種運作狀态資訊的指令行工具。它可以顯示本地或者遠端虛拟機程序中的類裝載、記憶體、垃圾回收、JIT編譯等運作資料,在沒有GUI圖形界面,隻是提供了純文字控制台環境的伺服器上,它将是運作期定位虛拟機性能問題的首選工具。

jstat的指令格式:

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

1. option: 參數選項

2. -t: 可以在列印的列加上Timestamp列,用于顯示系統運作的時間

3. -h: 可以在周期性資料資料的時候,可以在指定輸出多少行以後輸出一次表頭

4. vmid: Virtual Machine ID( 程序的 pid)

5. interval: 執行每次的間隔時間,機關為毫秒

6. count: 用于指定輸出多少次記錄,預設則會一直列印

7. 對于指令格式中的VMID和LVMID,如過是本地虛拟機程序,VMID和LVMID是一緻的,如 果是遠端虛拟機,那VMID的格式應當是:[protocol:] [//] lvmid[@hostname[:port]/servername]

8. 參數interval 和count分别表示查詢的間隔和次數,如果省略這兩個參數,說明隻查詢一次。

Jstat常用option選項:

選項 作用
-class 類裝載數量、解除安裝數量、總空間以及類狀态所消耗時間
-GC 監視Java堆容量狀況,包括Eden、Survivor、老年代、永久代等
-GCcapacity 監視Java堆最大、最小空間
-GCutil 關注已使用空間占總空間的百分比
-GCcause 類似GCutil,額外輸出上次GC的原因
-GCnew 新生代GC狀況
-GCnewcapacity 與-GCnew類似,輸出主要關注使用到的最大、最小空間
-GCold 老年代GC狀況
-GColdcapacity 與-GCold類似,輸出主要關注使用到的最大、最小空間
-GCpermcapacity 輸出永久代使用到的最大、最小空間
-compiler 輸出JIT編譯過的方法和耗時
-printcompilation 輸出已經被JIT編譯的方法
-GCmetacapacity 中繼資料空間統計

案例

加上-GC顯示将會GC堆資訊,使用上一個案例,設定VM參數為 -Xms30m -Xmx30m -Xmn10m ,即初始記憶體30m,最大記憶體30m,年輕代10m。

運作程式,使用 jstat -gc 6128 指令結果如下,可以看到GC堆資訊:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

S0C:年輕代中第一個Survivor(幸存區)的容量 (位元組)

S1C:年輕代中第二個Survivor(幸存區)的容量 (位元組)

S0U :年輕代中第一個Survivor(幸存區)目前已使用空間 (位元組)

S1U :年輕代中第二個Survivor(幸存區)目前已使用空間 (位元組)

EC :年輕代中Eden(伊甸園)的容量 (位元組)

EU :年輕代中Eden(伊甸園)目前已使用空間 (位元組)

OC :Old代的容量 (位元組)

OU :Old代目前已使用空間 (位元組)

MC:metaspace(元空間)的容量 (位元組)

MU:metaspace(元空間)目前已使用空間 (位元組)

YGC :從應用程式啟動到采樣時年輕代中GC次數

YGCT :從應用程式啟動到采樣時年輕代中GC所用時間(s)

FGC :從應用程式啟動到采樣時old代(全GC)GC次數

FGCT :從應用程式啟動到采樣時old代(全GC)GC所用時間(s)

GCT:從應用程式啟動到采樣時GC用的總時間(s)

從圖中可以看出,各項結果符合我們的VM參數設定的資訊。

2.5. jmap:Java記憶體映像工具

jmap指令用于生成堆轉儲快照。jmap的作用并不僅僅為了擷取dump檔案,它還可以查詢finalize執行隊列、java堆和永久代的詳細資訊。如空間使用率、目前用的是哪種收集器等。

jmap格式:

jmap [option] vmid

jmap常用option選項:

選項 作用
-dump 生成堆轉儲快照,格式為-dump:[live,]format=b,file=,不建議使用
-finalizerinfo 顯示在F-Queue中等待Finalizer線程執行finalize方法的對象
-heap 顯示java堆詳細資訊,回收器種類、參數配置、分代狀況等
-histo 顯示堆中對象統計資訊,包括類、執行個體數量、合計容量,會先觸發GC,再統計資訊,不建議使用
-permstat 檢視永久代記憶體狀态,比較耗時,會暫停應用,不建議使用

案例:

還是上面的例子。

使用jmap -heap 6128,可以看到我們的VM參數設定的資訊:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

生成dump檔案

jmap -dump:live,format=b,file=‪C:\Users\lx\Desktop\test1.bin 9472

将生成堆轉儲快照,這裡我生成到桌面。後面可以使用jhat分析dump檔案。

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

2.7. jstack:Java堆棧跟蹤工具

jstack指令用于生成虛拟機目前時刻的線程快照(一般稱為threaddump或者javacore檔案)。線程快照就是目前虛拟機内每一條線程正在執行的方法堆棧集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如線程死鎖、死循環、請求外部資源導緻長時間等待等。

jstack 格式:

jstack [option] vmid

jstack常見option選項:

選項 作用 案例
-m 如果調用本地方法,則顯示C/C++的堆棧 jstack -m 1479
-l 除堆棧外,顯示關于鎖的附加資訊 jstack -l 1479
-F 當正常輸出的請求不被響應時,強制輸出線程堆棧 jstack -F 1479

案例

jstack -l 9472

會輸出很多資訊,我們可以找到如下資訊:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

可以看到,main線程正在限時等待——因為sleep的原因。

jstack 可以幫助我們用來分析線程資訊,比如死鎖,狀态等。

2.8. 基礎工具總結

  1. jps将列印所有正在運作的 Java 程序。
  2. jstat允許使用者檢視目标 Java 程序的類加載、即時編譯以及垃圾回收相關的資訊。它常用于檢測垃圾回收問題以及記憶體洩漏問題。
  3. jmap允許使用者統計目标 Java 程序的堆中存放的 Java 對象,并将它們導出成二進制檔案。
  4. jinfo将列印目标 Java 程序的配置參數,并能夠改動其中 manageabe 的參數。
  5. jstack将列印目标 Java 程序中各個線程的棧軌迹、線程狀态、鎖狀況等資訊。它還将自動檢測死鎖。

三. 可視化故障處理工具

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

HSDB(Hotspot Debugger),是一款内置于 SA 中的 GUI 調試工具,可用于調試 JVM 運作時資料,進而進行故障排除。

3.1.1 HSDB發展

sa-jdi.jar

在 Java9 之前,JAVA_HOME/lib 目錄下有個 sa-jdi.jar,可以通過如下指令啟動HSDB(圖形界面)及CLHSDB(指令行)。

java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_301.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
           

sa-jdi.jar中的sa的全稱為 Serviceability Agent,它之前是sun公司提供的一個用于協助調試 HotSpot 的元件,而 HSDB 便是使用Serviceability Agent 來實作的。

由于Serviceability Agent 在使用的時候會先attach程序,然後暫停程序進行snapshot,最後deattach程序(程序恢複運作),是以在使用 HSDB 時要注意。

jhsdb

jhsdb 是 Java9 引入的,可以在 JAVA_HOME/bin 目錄下找到 jhsdb;它取代了 JDK9 之前的 JAVA_HOME/lib/sa-jdi.jar,可以通過下述指令來啟動 HSDB。

$ cd /Library/Java/JavaVirtualMachines/jdk-9.0.4.jdk/Contents/Home/bin/
$ jhsdb hsdb
           

jhsdb 有 clhsdb、debugd、hsdb、jstack、jmap、jinfo、jsnap 這些 mode 可以使用。

其中 hsdb 為 ui debugger,就是 jdk9 之前的 sun.jvm.hotspot.HSDB;而 clhsdb 即為 jdk9 之前的sun.jvm.hotspot.CLHSDB。

3.1.2 HSDB實操

3.1.2.1 啟動HSDB

檢測不同 JDK 版本需要使用不同的 HSDB 版本,否則容易出現無法掃描到對象等莫名其妙的問題。

Mac:JDK7 和 JDK8 均可以采用以下的方式

$ java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_301.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
           

如果執行報錯,則前面加上 sudo,或者更改 sa-jdi.jar 的權限。

sudo chmod -R 777 sa-jdi.jar 
           

本地安裝的是 JDK8,在啟動 HSDB 後,發現無法連接配接到 Java 程序,在 attach 過程中會提示如下錯誤:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

網上搜尋相關解決方案,建議更換 JDK 版本。可以去參考 Mac下安裝多個版本的JDK并随意切換

個人在配置的過程中遇到了這樣一個問題:在切換 JDK 版本時,發現不生效,網上各種查找方案,動手嘗試,最後都沒有成功。解決方案:手動修改 .bash_profile 檔案,增加注釋。

首次嘗試 JDK 11,但是還是無法 attach Java 程序,試了好久都不行,隻能再次嘗試 JDK9.

而 JDK9 的啟動方式有些差別

$ cd /Library/Java/JavaVirtualMachines/jdk-9.0.4.jdk/Contents/Home/bin/
$ jhsdb hsdb
           

其中啟動版本可以使用 /usr/libexec/java_home -V 擷取 HSDB 對 Serial GC 支援的較好,是以 Debug 時增加參數 -XX:+UseSerialGC。注意運作程式 Java 的版本和 hsdb 的 Java 版本要一緻才行。

注意:如果後續想要下載下傳 .class 檔案,啟動 hsdb 時,需要執行 sudo jhsdb hsdb 指令。

3.1.2.2 HSDB可視化界面

比如說有這麼一個 Java 程式,我們使用 Thread.sleep 方法讓其長久等待,然後擷取其程序 id。

public class InvokeTest {
      

  public static void printException(int num) {
      
    new Exception("#" + num).printStackTrace();
  }

  public static void main(String[] args)
      throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InterruptedException {
      
    Class<?> cl = Class.forName("InvokeTest");
    Method method = cl.getMethod("printException", int.class);
    for (int i = 1; i < 20; i++) {
      
      method.invoke(null, i);
      if (i == 17) {
      
        Thread.sleep(Integer.MAX_VALUE);
      }
    }
  }
}
           

然後在 terminal 視窗執行 jps 指令:

27995 InvokeTest
           

然後在 HSDB 界面點選 file 的 attach,輸入 pid,如果按照上述步驟操作,是可以操作成功的。

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

attach 成功後,效果如下所示:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

更多操作選擇推薦閱讀:解讀HSDB

3.1.2.3分析對象存儲區域

下面代碼中的 heatStatic、heat、heatWay 分别存儲在什麼地方呢?

package com.msdn.java.hotspot.hsdb;

public class Heat2 {
      

  private static Heat heatStatic = new Heat();
  private Heat heat = new Heat();

  public void generate() {
      
    Heat heatWay = new Heat();
    System.out.println("way way");
  }
}

class Heat{
      

}
           

測試類

package com.msdn.java.hotspot.hsdb;

public class HeatTest {
      
  public static void main(String[] args) {
      
    Heat2 heat2 = new Heat2();
    heat2.generate();
  }
}
           

關于上述問題,我們大概都知道該怎麼回答:

heatStatic 屬于靜态變量,引用應該是放在方法區中,對象執行個體位于堆中;

heat 屬于成員變量,在堆上,作為 Heat2 對象執行個體的屬性字段;

heatWay 屬于局部變量,位于 Java 線程的調用棧上。

那麼如何來看看這些變量在 JVM 中是怎麼存儲的?這裡借助 HSDB 工具來進行示範。

此處我們使用 IDEA 進行斷點調試,後續會再介紹 JDB 如何進行代碼調試。

IDEA 執行前需要增加 JVM 參數配置,HSDB 對 Serial GC 支援的較好,是以 Debug 時增加參數 -XX:+UseSerialGC;此外設定 Java Heap 為 10MB;UseCompressedOops 參數用來壓縮 64位指針,節省記憶體空間。關于該參數的詳細介紹,推薦閱讀本文。

最終 JVM 參數配置如下:

-XX:+UseSerialGC  -Xmn10M -XX:-UseCompressedOops
           
深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

然後在 Heat2 中的 System 語句處打上斷點,開始 debug 執行上述代碼。

接着打開指令行視窗執行 jps 指令檢視我們要調試的 Java 程序的 pid 是多少:

% jps
9977 HeatTest
           

接着我們按照上文講解啟動 HSDB,注意在 IDEA 中執行代碼時,Java 版本為 Java9,要與 HSDB 相關的 Java 版本一緻。

在 attach 成功後,選中 main線程并打開其棧資訊,接着打開 console 視窗,下面我将自測的指令及結果都列舉了出來,并簡要介紹其作用,以及可能遇到的問題。

首先執行 help 指令,檢視所有可用的指令

hsdb> help
Available commands:
  assert true | false
  attach pid | exec core
  buildreplayjars [ all | app | boot ]  | [ prefix ]
  detach
  dis address [length]
  disassemble address
  dumpcfg {
       -a | id }
  dumpcodecache
  dumpideal {
       -a | id }
  dumpilt {
       -a | id }
  dumpreplaydata {
       <address > | -a | <thread_id> }
  echo [ true | false ]
  examine [ address/count ] | [ address,address]
  field [ type [ name fieldtype isStatic offset address ] ]
  findpc address
  flags [ flag | -nd ]
  help [ command ]
  history
  inspect expression
  intConstant [ name [ value ] ]
  jdis address
  jhisto
  jstack [-v]
  livenmethods
  longConstant [ name [ value ] ]
  pmap
  print expression
  printall
  printas type expression
  printmdo [ -a | expression ]
  printstatics [ type ]
  pstack [-v]
  quit
  reattach
  revptrs address
  scanoops start end [ type ]
  search [ heap | perm | rawheap | codecache | threads ] value
  source filename
  symbol address
  symboldump
  symboltable name
  thread {
       -a | id }
  threads
  tokenize ...
  type [ type [ name super isOop isInteger isUnsigned size ] ]
  universe
  verbose true | false
  versioncheck [ true | false ]
  vmstructsdump
  where {
       -a | id }
  
  hsdb> where 3587
Thread 3587 Address: 0x00007fb25c00a800

Java Stack Trace for main
Thread state = BLOCKED
 - public void generate() @0x0000000116953ff8 @bci = 8, line = 15, pc = 0x0000000123cdacd7, oop = 0x000000013316f128 (Interpreted)
 - public static void main(java.lang.String[]) @0x00000001169539b0 @bci = 9, line = 11, pc = 0x0000000123caf4ba (Interpreted)

hsdb> 
           

3.1.2.4 主要指令簡介

指令1、universe 指令來檢視GC堆的位址範圍和使用情況,可以看到我們建立的三個對象都是在 eden 區。因為使用的是 Java9,是以已經不存在 Perm gen 區了,

hsdb> universe
Heap Parameters:
Gen 0:   eden [0x0000000132e00000,0x000000013318c970,0x0000000133600000) space capacity = 8388608, 44.36473846435547 used
  from [0x0000000133600000,0x0000000133600000,0x0000000133700000) space capacity = 1048576, 0.0 used
  to   [0x0000000133700000,0x0000000133700000,0x0000000133800000) space capacity = 1048576, 0.0 usedInvocations: 0

Gen 1:   old  [0x0000000133800000,0x0000000133800000,0x0000000142e00000) space capacity = 257949696, 0.0 usedInvocations: 0
           

不借助指令的話,還可以這樣操作來檢視。

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

指令2、scanoops 檢視類型

Java 代碼裡,執行到 System 輸出語句時應該建立了3個 Heat 的執行個體,它們必然在 GC 堆裡,但都在哪裡,可以用scanoops指令來看:

hsdb> scanoops 0x0000000132e00000 0x000000013318c970 com.msdn.java.hotspot.hsdb.Heat
0x000000013316f118 com/msdn/java/hotspot/hsdb/Heat
0x000000013316f140 com/msdn/java/hotspot/hsdb/Heat
0x000000013316f150 com/msdn/java/hotspot/hsdb/Heat
           

scanoops 接受兩個必選參數和一個可選參數:必選參數是要掃描的位址範圍,一個是起始位址一個是結束位址;可選參數用于指定要掃描什麼類型的對象執行個體。實際掃描的時候會掃出指定的類型及其派生類的執行個體。

從 universe 指令傳回結果可知,對象是在 eden 裡配置設定的記憶體(注意used),是以執行 scanoops 指令時位址範圍可以從 eden 中擷取。

指令3、findpc 指令可以進一步知道這些對象都在 eden 之中配置設定給 main 線程的

thread-local allocation buffer (TLAB)中

網上的多數文章都介紹 whatis 指令,不過我個人在嘗試的過程中執行該指令報錯,如下述所示:

hsdb> whatis 0x000000012736efe8
Unrecognized command.  Try help...
           

指令不行,那麼換種思路,使用 HSDB 可視化視窗來檢視對象的位址資訊。

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

至于為什麼無法使用 whatis 指令,原因是 Java9 的 HSDB 已經沒有 whatis 指令了,取而代之的是 findpc 指令。

hsdb> findpc 0x000000013316f118
Address 0x000000013316f118: In thread-local allocation buffer for thread "main" (3587)  [0x00000001331639f8,0x000000013316f160,0x000000013318c730,{
      0x000000013318c970})
           

指令4、inspect指令來檢視對象的内容:

hsdb> inspect 0x000000013316f118
instance of Oop for com/msdn/java/hotspot/hsdb/Heat @ 0x000000013316f118 @ 0x000000013316f118 (size = 16)
_mark: 1
_metadata._klass: InstanceKlass for com/msdn/java/hotspot/hsdb/Heat
           

可見一個 heatStatic 執行個體要16位元組。因為 Heat 類沒有任何 Java 層的執行個體字段,這裡就沒有任何 Java 執行個體字段可顯示。

或者通過可視化工具來檢視:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

一個 Heat 的執行個體包含 2個給 VM 用的隐含字段作為對象頭,和0個Java字段。

對象頭的第一個字段是mark word,記錄該對象的GC狀态、同步狀态、identity hash code之類的多種資訊。

對象頭的第二個字段是個類型資訊指針,klass pointer。這裡因為預設開啟了壓縮指針,是以本來應該是64位的指針存在了32位字段裡。

最後還有4個位元組是為了滿足對齊需求而做的填充(padding)。

指令5、mem指令來看實際記憶體裡的資料格式

我們執行 help 時發現已經沒有 mem 指令了,那麼現在隻能通過 HSDB 可視化工具來擷取資訊。

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

關于這塊的講解可以參考 R大的文章,文章中講述還是使用 mem 指令,格式如下:mem 0x000000013316f118 2

mem 指令接受的兩個參數都必選,一個是起始位址,另一個是以字寬為機關的“長度”。

雖然我們通過 inspect 指令是知道 Heat 執行個體有 16 位元組,為什麼給2暫不可知。

在實踐的過程中,發現了一個類似的指令:

hsdb> examine 0x000000013316f118/2
0x000000013316f118: 0x0000000000000001 0x0000000116954620
           

指令6、revptrs 反向指針

JVM 通過引用來定位堆上的具體對象,有兩種實作方式:句柄池和直接指針。目前 Java 預設使用的 HotSpot 虛拟機采用的便是直接指針進行對象通路的。

我們在執行 Java 程式時加了 UseCompressedOops 參數,即使不加,Java9 也會預設開啟壓縮指針。啟用“壓縮指針”的功能把64位指針壓縮到隻用32位來存。壓縮指針與非壓縮指針直接有非常簡單的1對1對應關系,前者可以看作後者的特例。關于壓縮指針,感興趣的朋友可以閱讀本文。

于是我們要找 heatStatic、heat、heatWay 這三個變量,等同于找出存有指向上述3個 Heat 執行個體的位址的存儲位置。

不嫌麻煩的話手工掃描記憶體去找也能找到,不過幸好HSDB内建了revptrs指令,可以找出“反向指針”——如果a變量引用着b對象,那麼從b對象出發去找a變量就是找一個“反向指針”。

hsdb> revptrs 0x000000013316f118
null
Oop for java/lang/Class @ 0x000000013316d660
           

确實找到了一個 Heat 執行個體的指針,在一個 java.lang.Class 的執行個體裡。

用 findpc 指令來看看這個Class對象在哪裡:

hsdb> findpc 0x000000013316d660
Address 0x000000013316d660: In thread-local allocation buffer for thread "main" (3587)  [0x00000001331639f8,0x000000013316f160,0x000000013318c730,{
      0x000000013318c970})
           

可以看到這個 Class 對象也在 eden 裡,具體來說在 main 線程的 TLAB 裡。

這個 Class 對象是如何引用到 Heat 的執行個體的呢?再用 inspect 指令:

hsdb> inspect 0x000000013316d660
instance of Oop for java/lang/Class @ 0x000000013316d660 @ 0x000000013316d660 (size = 184)
<<Reverse pointers>>: 
heatStatic: Oop for com/msdn/java/hotspot/hsdb/Heat @ 0x000000013316f118 Oop for com/msdn/java/hotspot/hsdb/Heat @ 0x000000013316f118
           

可以看到,這個 Class 對象裡存着 Heat 類的靜态變量 heatStatic,指向着第一個 Heat 執行個體。注意該對象沒有對象頭。

靜态變量按照定義存放在方法區,雖然 Java 虛拟機規範把方法區描述為堆的一個邏輯部分,但是它卻有一個别名叫做 Non-Heap(非堆)。但現在在 JDK7 的 HotSpot VM 裡它實質上也被放在 Java heap 裡了。可以把這種特例看作是 HotSpot VM 把方法區的一部分資料也放在 Java heap 裡了。

通過可視化工具操作也可以得到上述結果:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

最終得到同樣的結果:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

同理,我們查找一下第二個變量 heat 的存儲資訊。

hsdb> revptrs 0x000000013316f140
null
Oop for com/msdn/java/hotspot/hsdb/Heat2 @ 0x000000013316f128
hsdb> findpc 0x000000013316f128
Address 0x000000013316f128: In thread-local allocation buffer for thread "main" (3587)  [0x00000001331639f8,0x000000013316f160,0x000000013318c730,{
      0x000000013318c970})
hsdb> inspect 0x000000013316f128
instance of Oop for com/msdn/java/hotspot/hsdb/Heat2 @ 0x000000013316f128 @ 0x000000013316f128 (size = 24)
<<Reverse pointers>>: 
_mark: 1
_metadata._klass: InstanceKlass for com/msdn/java/hotspot/hsdb/Heat2
heat: Oop for com/msdn/java/hotspot/hsdb/Heat @ 0x000000013316f140 Oop for com/msdn/java/hotspot/hsdb/Heat @ 0x000000013316f140
           

接着來找第三個變量 heatWay:

hsdb> revptrs 0x000000013316f150
null
null
           

回到我們的 HSDB 可視化界面,可以發現如下資訊:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

Stack Memory 視窗的内容有三欄:

  • 左起第1欄是記憶體位址,提醒一下本文裡提到“記憶體位址”的地方都是指虛拟記憶體意義上的位址,不是“實體記憶體位址”,不要弄混了這倆概念;
  • 第2欄是該位址上存的資料,以字寬為機關
  • 第3欄是對資料的注釋,豎線表示範圍,橫線或斜線連接配接範圍與注釋文字。

仔細看會發現那個視窗裡正好就有 0x000000013316f150 這數字,位于 0x00007000068e29e0 位址上,而這恰恰對應 main 線程上 generate()的棧桢。

3.2. JConsole:Java監視與管理控制台

3.2.1. jconsole簡介

JConsole(java monitoring and management console)是一款基于JMX的可視化監視和管理工具。

3.2.2. 啟動JConsole

  • 點選JDK/bin 目錄下面的“jconsole.exe”即可啟動
  • 然後會自動自動搜尋本機運作的所有虛拟機程序
  • 選擇其中一個程序可開始進行監控
深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

3.2.3. JConsole基本介紹

JConsole 基本包括以下基本功能:概述、記憶體、線程、類、VM概要、MBean

運作下面的程式、然後使用JConsole進行監控;注意設定虛拟機參數

package com.jvm.jconsole;
import java.util.ArrayList;
import java.util.List;
/**
 * 設定虛拟機參數:-Xms100M -Xms100m -XX:+UseSerialGC -XX:+PrintGCDetails
 */
public class Demo1 {
    static class OOMObject {
        public byte[] placeholder = new byte[64 * 1024];
    }
    public static void fillHeap(int num) throws InterruptedException {
        Thread.sleep(20000); //先運作程式,在執行監控
        List<OOMObject> list = new ArrayList<OOMObject>();
        for (int i = 0; i < num; i++) {
            // 稍作延時,令監視曲線的變化更加明顯
            Thread.sleep(50);
            list.add(new OOMObject());
        }
        System.gc();
    }
    public static void main(String[] args) throws Exception {
        fillHeap(1000);
        while (true) {
            //讓其一直運作着
        }
    }
}           
深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全
  • 打開JConsole檢視上面程式
深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全
  • 可以切換頂部的頁籤檢視各種名額資訊。
深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

3.2.4. 記憶體監控

“記憶體”頁簽相當于可視化的jstat 指令,用于監視受收集器管理的虛拟機記憶體的變換趨勢。

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全
  • 代碼運作,控制台也會輸出gc日志:
[GC (Allocation Failure) [DefNew: 27328K->3392K(30720K), 0.0112139 secs] 27328K->19901K(99008K), 0.0112664 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] 
[GC (Allocation Failure) [DefNew: 30720K->3392K(30720K), 0.0133413 secs] 47229K->40117K(99008K), 0.0133708 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew: 30664K->3374K(30720K), 0.0140975 secs] 67389K->65091K(99008K), 0.0141239 secs] [Times: user=0.00 sys=0.02, real=0.01 secs] 
[Full GC (System.gc()) [Tenured: 61716K->66636K(68288K), 0.0098835 secs] 66919K->66636K(99008K), [Metaspace: 9482K->9482K(1058816K)], 0.0100578 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]           

3.2.5. 線程監控

如果上面的“記憶體”頁簽相當于可視化的jstat指令的話,“線程”頁簽的功能相當于可視化的jstack指令,遇到線程停頓時可以使用這個頁簽進行監控分析。線程長時間停頓的主要原因主要有:等待外部資源(資料庫連接配接、網絡資源、裝置資源等)、死循環、鎖等待(活鎖和死鎖)

下面三個方法分别等待控制台輸入、死循環示範、線程鎖等待示範

  • 第一步:運作如下代碼:
package com.jvm.jconsole;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Demo2 {
    public static void main(String[] args) throws IOException {
        waitRerouceConnection();
        createBusyThread();
        createLockThread(new Object());
    }
    /**
     * 等待控制台輸入
     *
     * @throws IOException
     */
    public static void waitRerouceConnection() throws IOException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
                try {
                    br.readLine();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }, "waitRerouceConnection");
        thread.start();
    }
    /**
     * 線程死循環示範
     */
    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();
    }
}           
  • 第二步:打開jconsole中檢視上面程式運作情況,可以檢視到3個目标線程
深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全
  • 第三步:檢視目标線程資訊

waitRerouceConnection線程處于讀取資料狀态,如下圖:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

testBusyThread線程位于代碼45行,處于運作狀态,如下圖:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全
    • testLockThread處于活鎖等待狀态,如下圖:
深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

隻要lock對象的notify()或notifyAll()方法被調用,這個線程便可能激活以繼續執行

3.2..6. 線程死鎖示範

第一步:運作下面代碼:

package com.jvm.jconsole;

public class Demo3 {
    public static void main(String[] args) {
        User u1 = new User("u1");
        User u2 = new User("u2");
        Thread thread1 = new Thread(new SynAddRunalbe(u1, u2, 1, 2, true));
        thread1.setName("thread1");
        thread1.start();
        Thread thread2 = new Thread(new SynAddRunalbe(u1, u2, 2, 1, false));
        thread2.setName("thread2");
        thread2.start();
    }
    /**
     * 線程死鎖等待示範
     */
    public static class SynAddRunalbe implements Runnable {
        User u1, u2;
        int a, b;
        boolean flag;
        public SynAddRunalbe(User u1, User u2, int a, int b, boolean flag) {
            this.u1 = u1;
            this.u2 = u2;
            this.a = a;
            this.b = b;
            this.flag = flag;
        }
        @Override
        public void run() {
            try {
                if (flag) {
                    synchronized (u1) {
                        Thread.sleep(100);
                        synchronized (u2) {
                            System.out.println(a + b);
                        }
                    }
                } else {
                    synchronized (u2) {
                        Thread.sleep(100);
                        synchronized (u1) {
                            System.out.println(a + b);
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static class User {
        private String name;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public User(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
}           

thread1持有u1的鎖,thread2持有u2的鎖,thread1等待擷取u2的鎖,thread2等待擷取u1的鎖,互相需要擷取的鎖都被對方持有者,造成了死鎖。程式中出現了死鎖的情況,我們是比較難以發現的。需要依靠工具解決。剛好jconsole就是這個美妙的工具。

第二步:在jconsole中打開上面程式的監控資訊:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

從上面可以看出代碼43行和50行處導緻了死鎖。

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

關于程式死鎖的,我們還可以使用指令行工具jstack來檢視java線程堆棧資訊,也可以發現死鎖。

3.3. JVisualVM:多合一故障處理工具

3.3.1. VisualVM介紹

VisualVM 是一款免費的,內建了多個 JDK 指令行工具的可視化工具,它能為您提供強大的分析能力,對 Java 應用程式做性能分析和調優。這些功能包括生成和分析海量資料、跟蹤記憶體洩漏、監控垃圾回收器、執行記憶體和 CPU 分析,同時它還支援在 MBeans 上進行浏覽和操作。本文主要介紹如何使用 VisualVM 進行性能分析及調優。

VisualVM位于{JAVA_HOME}/bin目錄中。

點選運作,效果如下:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

3.3.2. 檢視jvm配置資訊

  • 第一步:點選左邊視窗顯示正在運作的java程序
深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全
  • 第二步:點選右側視窗“概述”,可以檢視各種配置資訊
深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

通過jdk提供的jinfo指令工具也可以檢視上面的資訊。

3.3.3. 檢視cpu、記憶體、類、線程監控資訊

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

3.3.4. 檢視堆的變化

步驟一:運作下面的代碼

  • 每隔3秒,堆記憶體使用新增100M
package com.jvm.visualvm;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

public class Demo1 {
    public static final int _1M = 1024 * 1024;
    public static void main(String[] args) throws InterruptedException {
        List<Object> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            list.add(new byte[100 * _1M]);
            TimeUnit.SECONDS.sleep(3);
            System.out.println(i);
        }
    }
}           

步驟二:在VisualVM可以很清晰的看到堆記憶體變化資訊。

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

3.3.5. 檢視堆快照

步驟一:點選“監視”->”堆(dump)”可以生産堆快照資訊.

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

步驟二:生成了以heapdump開頭的一個頁籤,内容如下:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

對于“堆 dump”來說,在遠端監控jvm的時候,VisualVM是沒有這個功能的,隻有本地監控的時候才有。

3.3.6. 導出堆快照檔案

步驟一:檢視堆快照,此步驟可以參考上面的“檢視堆快照”功能

步驟二:右鍵點選另存為,即可導出hprof堆快照檔案,可以發給其他同僚分析使用

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全
深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

3.3.7. 檢視class對象加載資訊

其次來看下永久保留區域PermGen使用情況

步驟一:運作一段類加載的程式,代碼如下:

package com.jvm.visualvm;
import java.io.File;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class Demo2 {
    private static List<Object> insList = new ArrayList<Object>();
    public static void main(String[] args) throws Exception {
        permLeak();
    }
    private static void permLeak() throws Exception {
        for (int i = 0; i < 2000; i++) {
            URL[] urls = getURLS();
            URLClassLoader urlClassloader = new URLClassLoader(urls, null);
            Class<?> logfClass = Class.forName("org.apache.commons.logging.LogFactory", true, urlClassloader);
            Method getLog = logfClass.getMethod("getLog", String.class);
            Object result = getLog.invoke(logfClass, "TestPermGen");
            insList.add(result);
            System.out.println(i + ": " + result);
            if (i % 100 == 0) {
                TimeUnit.SECONDS.sleep(1);
            }
        }
    }
    private static URL[] getURLS() throws MalformedURLException {
        File libDir = new File("D:\\installsoft\\maven\\.m2\\repository3.3.9_0\\commons-logging\\commons-logging\\1.1.1");
        File[] subFiles = libDir.listFiles();
        int count = subFiles.length;
        URL[] urls = new URL[count];
        for (int i = 0; i < count; i++) {
            urls[i] = subFiles[i].toURI().toURL();
        }
        return urls;
    }
}           

步驟二:打開visualvm檢視,metaspace

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

3.3.8. CPU分析:發現cpu使用率最高的方法

CPU 性能分析的主要目的是統計函數的調用情況及執行時間,或者更簡單的情況就是統計應用程式的 CPU 使用情況。

沒有程式運作時的 CPU 使用情況如下圖:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

步驟一:運作下列程式:

package com.jvm.visualvm;

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        cpuFix();
    }
    /**
    * cpu 運作固定百分比
    *
    * @throws InterruptedException
    */
    public static void cpuFix() throws InterruptedException {
        // 80%的占有率
        int busyTime = 8;
        // 20%的占有率
        int idelTime = 2;
        // 開始時間
        long startTime = 0;
        while (true) {
            // 開始時間
            startTime = System.currentTimeMillis();
            /*
            * 運作時間
            */
            while (System.currentTimeMillis() - startTime < busyTime) {
                ;
            }
            // 休息時間
            Thread.sleep(idelTime);
        }
    }
}           

步驟二:打開visualvm檢視cpu使用情況,我的電腦是8核的,如下圖:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

過高的 CPU 使用率可能是我們的程式代碼性能有問題導緻的。可以切換到“抽樣器”對cpu進行采樣,可以擦看到那個方法占用的cpu最高,然後進行優化。

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

從圖中可以看出cpuFix方法使用cpu最多,然後就可以進行響應的優化了。

3.3.9. 檢視線程快照:發現死鎖問題

Java 語言能夠很好的實作多線程應用程式。當我們對一個多線程應用程式進行調試或者開發後期做性能調優的時候,往往需要了解目前程式中所有線程的運作狀态,是否有死鎖、熱鎖等情況的發生,進而分析系統可能存在的問題。

在 VisualVM 的監視标簽内,我們可以檢視目前應用程式中所有活動線程(Live threads)和守護線程(Daemon threads)的數量等實時資訊。

可以檢視線程快照,發現系統的死鎖問題。

步驟一:運作下面的代碼:

package com.jvm.visualvm;

public class Demo4 {
    public static void main(String[] args) {
        Obj1 obj1 = new Obj1();
        Obj2 obj2 = new Obj2();
        Thread thread1 = new Thread(new SynAddRunalbe(obj1, obj2, 1, 2, true));
        thread1.setName("thread1");
        thread1.start();
        Thread thread2 = new Thread(new SynAddRunalbe(obj1, obj2, 2, 1, false));
        thread2.setName("thread2");
        thread2.start();
    }
    /**
    * 線程死鎖等待示範
    */
    public static class SynAddRunalbe implements Runnable {
        Obj1 obj1;
        Obj2 obj2;
        int a, b;
        boolean flag;
        public SynAddRunalbe(Obj1 obj1, Obj2 obj2, int a, int b, boolean flag) {
            this.obj1 = obj1;
            this.obj2 = obj2;
            this.a = a;
            this.b = b;
            this.flag = flag;
        }
        @Override
        public void run() {
            try {
                if (flag) {
                    synchronized (obj1) {
                        Thread.sleep(100);
                        synchronized (obj2) {
                            System.out.println(a + b);
                        }
                    }
                } else {
                    synchronized (obj2) {
                        Thread.sleep(100);
                        synchronized (obj1) {
                            System.out.println(a + b);
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static class Obj1 {
    }
    public static class Obj2 {
    }
}           

程式中:thread1持有obj1的鎖,thread2持有obj2的鎖,thread1等待擷取obj2的鎖,thread2等待擷取obj1的鎖,互相需要擷取的鎖都被對方持有者,造成了死鎖。程式中出現了死鎖的情況,我們是比較難以發現的。需要依靠工具解決。

步驟二:打開visualvm檢視堆棧資訊:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

點選dump,生成線程堆棧資訊:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

可以看到“Found one Java-level deadlock”,包含了導緻死鎖的代碼。

"thread2":
    at com.jvm.visualvm.Demo4$SynAddRunalbe.run(Demo4.java:50)
    - waiting to lock <0x00000007173d40f0> (a com.jvm.visualvm.Demo4$Obj1)
    - locked <0x00000007173d6310> (a com.jvm.visualvm.Demo4$Obj2)
    at java.lang.Thread.run(Thread.java:745)
"thread1":
    at com.jvm.visualvm.Demo4$SynAddRunalbe.run(Demo4.java:43)
    - waiting to lock <0x00000007173d6310> (a com.jvm.visualvm.Demo4$Obj2)
    - locked <0x00000007173d40f0> (a com.jvm.visualvm.Demo4$Obj1)
    at java.lang.Thread.run(Thread.java:745)           

上面這段資訊可以看出,thread1持有Obj1對象的鎖,等待擷取Obj2的鎖,thread2持有Obj2的鎖,等待擷取Obj1的鎖,導緻了死鎖。

3.3.10. 總結

本文介紹了jdk提供的一款非常強大的分析問題的一個工具VisualVM,通過他,我們可以做一下事情:

  • 檢視應用jvm配置資訊
  • 檢視cpu、記憶體、類、線程監控資訊
  • 檢視堆的變化
  • 檢視堆快照
  • 導出堆快照檔案
  • 檢視class對象加載資訊
  • CPU分析:發現cpu使用率最高的方法
  • 分析死鎖問題,找到死鎖的代碼

3.4. Java Mission Control(JMC):可持續線上的監控工具

3.4.1.JMC的組成

使用 JMC可以監視和管理 Java 應用程式,不會導緻相關工具類的大幅度性能開銷,它使用為 Java 虛拟機 (JVM) 的普通自适應動态優化收集的資料。

主要部分

  • JVM浏覽器:顯示了正在運作的 Java 應用程式及其 JVM,每個JVM執行個體稱為一個JVM連接配接。JVM浏覽器允許使用者列出并連接配接到本地和遠端運作的 Java 應用。它能夠使用 Java 發現協定(JDP)自動地發現本地和遠端運作的 Java 程序。
  • JMX 控制台:能夠通過 JMX 接口管理并監控 JDK ,實時收集并顯示其特征。它提供了實時集合、堆使用情況、CPU 負載以及其他通過 MBeans 暴露的和在 MBean 伺服器中注冊的資訊,并可通過托管 Bean (MBean) 更改一些運作時屬性。還可以建立在特定事件上觸發的規則 (例如,如果應用程式的 CPU 占用率達到了 90%,則發送電子郵件)。
  • JFR:提供了一種從作業系統層、JVM 和 Java 應用程式層收集事件的方式。收集的事件包括線程延時事件,例如休眠(sleep)、等待(wait)、鎖競争、I/O、GC 和方法分析。
Java Mission Control 插件使用 Java Management Extensions (JMX) 代理連接配接到 JVM

啟動JMC後,連接配接某個本地應用後,出現如下界面:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

遠端連接配接JVM(通過JMX連接配接如果想要用jmc監控遠端的JVM程序,配置方式和jvisualvm方式一一樣即可)

本地連接配接比較簡單這裡就不在贅述,遠端連接配接JVM,這裡利用VMWare工具進行模拟,過程中遇到一些問題,值得注意的。

首先,遠端機器被監控的程式需要開啟調試端口,在執行java指令行中加入以下屬性,屬性沒有以ssl安全認證方式連接配接的,案例中啟動監聽端口為7091

3.4.2JMX配置(被監控的遠端Tomcat)

進入tomcat安裝目錄安裝找到catalina.sh檔案,在CATALINA_OPTS中增加一下配置:

-Dcom.sun.management.jmxremote=true 
-Djava.rmi.server.hostname=115.29.206.6 
-Dcom.sun.management.jmxremote.port=6666 
-Dcom.sun.management.jmxremote.ssl=false 
-Dcom.sun.managementote.ssl=false 
-Dcom.sun.management.jmxremote.authenticate=false 

-XX:+UnlockCommercialFeatures -XX:+FlightRecorder
           

配置成功之後我的CATALINA_OPTS為:

CATALINA_OPTS="-Xms1024m -Xmx6144m -XX:+HeapDumpOnOutOfMemoryError 
-XX:+PrintGCDetails -XX:+PrintGCDateStamps 
-Dspring.profiles.active=production 
-Xloggc:/data/logs/gc-`date +"%Y-%m-%d_%H%M%S"`.log 
-XX:MaxPermSize=1024M 
-Dcom.sun.management.jmxremote=true 
-Djava.rmi.server.hostname=115.29.206.6 
-Dcom.sun.management.jmxremote.port=6666 
-Dcom.sun.management.jmxremote.ssl=false 
-Dcom.sun.managementote.ssl=false 
-Dcom.sun.management.jmxremote.authenticate=false 
-XX:+UnlockCommercialFeatures -XX:+FlightRecorder"
           

3.4.3主要配置項說明:

  • -Djava.rmi.server.hostname=115.29.206.6:這個配置的值是遠端tomcat伺服器的外網ip。
  • -Dcom.sun.management.jmxremote.port=6666:這個是對外開放的端口,後面在配置用戶端的時候需要用到這個。
深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

建立完成後輕按兩下MBean或者右鍵–>打開JMX控制台,均能打開控制台

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

3.4.4展示面闆

3.4.4.1概覽

預設布局提供 CPU 和記憶體使用情況的概覽。

概覽:可以添加自定義圖表,通過概覽的加号”添加圖表”實作;可以重置,通過”重置為預設控件”實作。

添加圖表後,可以通過圖表的加号添加相應的子項,并可以在圖表上右鍵詳細設定(如下圖中的Test)

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

3.4.4.2 MBean浏覽器

JMC使用托管Bean (MBean) 為監視和管理 Java 應用程式性能提供統一且一緻的界面。MBean 是采用符合 JMX 規範的設計模式的托管對象。MBean 可以表示一個裝置、一個應用程式或需要托管的任何資源。MBean 的管理界面由一組屬性、操作和通知組成。

MBean 浏覽器提供對所有已注冊 MBean 的通路。MBean 在管理伺服器中注冊,後者可以通過與 Java Management Extensions (JMX) 相容的客戶機通路

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

要建立和注冊新 MBean,請單擊 MBean 面闆加号圖示。執行此操作會啟動動态建立和注冊新的 MBean 向導,提示為新 MBean 輸入對象名和類名。若要繼續,對象名必須有效,并且類名必須是有效的 Java 類名。請注意,該向導不會驗證類是否對 MBean 伺服器可用;将隻進行文法檢查。要登出特定 MBean,右鍵單擊并從上下文菜單中選擇登出。

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

3.4.4.3 MBean功能

  • 屬性:列出所選 MBean 的屬性。
  • 操作:列出可從所選 MBean 調用的操作。
  • 通知:列出在運作時期間 MBean 所提示的通知。
  • 中繼資料:包含描述 MBean 的資訊。

3.4.4.4 觸發器

使用觸發器頁籤可以管理滿足特定條件時觸發事件的規則。這是一種無需持續地監視應用程式即可跟蹤運作時問題的有用方法。以灰色顯示的規則在監視的 JVM 中不可用。預設情況下,停用所有規則。要激活某個規則,請選中該規則旁邊的複選框

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

規則詳細資訊

  • 條件:選擇觸發規則時所在的屬性和值。
  • 操作:選擇應由規則觸發的事件。
  • 限制條件:選擇激活規則時的時間限制條件。

建立觸發器

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

設定觸發規則的條件:

  • 最大觸發值:最大觸發值(對于數值屬性) 或比對字元串 (對于非數值屬性),指定用于觸發規則的標明屬性的最大值或比對字元串。
  • 持續時間:指定條件必須保持為“真”以觸發規則的持續時間 (預設秒)。
  • 限制時間段:指定可以接下來再次觸發規則所要經過的最短時間長度 (預設秒)。
  • 滿足條件時觸發:選擇是否在滿足條件時觸發規則。例如,如果觸發值設定為 100,則在值從小于 100 更改為 100 或更高值時觸發規則。
  • 從條件恢複時觸發:選擇是否當不再滿足條件時觸發規則。例如,如果觸發值設定為 100,則在值從 100 或更高值更改為 100 時觸發規則。
深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

在規則觸發時發生的操作

  • 應用程式預警:在JMC中顯示預警對話框。
  • 控制台輸出:将通知發送到啟動 JMX 控制台的控制台 (指令提示) 的标準輸出 (stdout)。
  • 轉儲飛行記錄:将最後指定時段 (秒) 的飛行記錄中可用的全部資料轉儲到 JFR 檔案。
  • HPROF 轉儲:啟動到指定 HPROF 檔案的記憶體轉儲。
  • 調用診斷指令:調用指定的診斷指令,通過附加或覆寫結果将輸出記錄到指定的 LOG 檔案。
  • 記錄到檔案:将通知寫入到指定的 TXT 日志檔案。
  • 發送電子郵件:通過電子郵件發送通知。可以配置 SMTP 伺服器位址和端口、電子郵件的接收方和發送方并提供 SSL 身份證明 (如果需要安全連接配接)。
  • 啟動連續飛行記錄:啟動連續飛行記錄。
  • 啟動限時飛行記錄:啟動飛行記錄,在指定的時段之後将結果轉儲到 JFR 檔案中。

3.4.4.5系統

系統頁籤提供了運作 JVM 的系統的資訊、JVM 的性能屬性以及系統屬性清單。

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全
  • 伺服器資訊

    在系統頁籤頂部的伺服器資訊面闆中,包含運作 JVM 的伺服器的類别和值清單。這些資訊對調試應用程式開發問題和運作時問題以及提出支援請求非常有用。這是一般系統資訊,不能更改。

  • JVM 統計資訊

    JVM 統計資訊面闆包含 JVM 主要性能屬性的目前值。預設情況下,表中顯示以下屬性:

  • 目前已加載類計數

    運作時間

要向表中添加屬性,請單擊 JVM 統計資訊面闆右上角的添加屬性按鈕。要删除屬性,請在表中右鍵單擊該屬性,然後選擇删除。右鍵單擊屬性後,可以更改其更新間隔、機關,而對于一些屬性,還可以設定值。
  • 系統屬性

    系統屬性面闆包含一個表,其中列出使用 JVM 調用的所有屬性的關鍵字和值。要篩選屬性,請選擇篩選列 (關鍵字或值),并在表上方的文本字段中指定篩選器字元串。

    要配置表外觀,請單擊系統屬性面闆右上角的表設定按鈕,然後選擇要顯示或隐藏的列。對于各列,可以設定最小寬度、比例和初始排序順序。展示的是一般系統資訊,不能更改。

3.4.4.6記憶體

使用記憶體頁籤可以監視應用程式使用記憶體資源的效率。此頁籤主要提供以下方面的資訊:堆使用量、垃圾收集和活動記憶體池。此頁籤上提供的資訊可幫助确定是否已将 JVM 配置為提供最佳應用程式性能。

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全
在記憶體頁籤中,可以使用該頁籤右上角的運作完全垃圾收集按鈕手動啟動完全垃圾收集。
  • GC 表

    GC 表面闆包含可用垃圾收集器的主要性能屬性的目前值。該面闆分為标有活動垃圾收集器名稱的多個頁籤

  • 總收集時間
  • 收集計數
  • GC 開始時間
  • GC 結束時間
  • GC 持續時間
  • GC ID
  • GC 線程計數
右鍵單擊屬性後,可以更改其更新間隔、機關,而對于一些屬性,還可以設定值。
  • 活動記憶體池

    活動記憶體池面闆包含一個表,其中列出可供 JVM 使用的記憶體池的資訊。預設情況下,該表包含以下列:

  • 池名稱:記憶體池的名稱。
  • 類型:記憶體池的類型。如果記憶體池屬于 Java 堆,則類型為 HEAP,否則為 NON_HEAP。
  • 已用:目前已用的記憶體池大小。
  • 最大值:記憶體池的最大大小。
  • 占用率:目前使用量占最大記憶體池大小的百分比。
  • 已用峰值:在受監視 JVM 的有效期内記憶體池已用記憶體的峰值。
  • 最大值峰值:在受監視 JVM 的有效期内最大記憶體池大小的峰值。

3.4.4.7線程

使用線程頁籤可以監視線程活動。此頁籤包含一個繪制應用程式随時間推移的活動線程使用情況的圖形、一個由該應用程式使用的所有活動線程的表以及標明線程的堆棧跟蹤。

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全
  • 活動線程圖

    活動線程圖顯示 Java 應用程式随時間推移啟動的線程數。預設情況下,圖形中顯示以下屬性:

    • 高峰活動線程計數
    • 活動線程總計數
    • 守護程式活動線程計數
  • 活動線程

    活動線程面闆包含一個表,其中列出 Java 應用程式所啟動活動線程的資訊。預設情況下,該表包含以下列:

    • 線程名稱:線程的名稱。
    • 線程狀态:線程的狀态。線程可以是以下狀态之一:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING 或 TERMINATED。
    • 受阻計數:線程處于 BLOCKED 狀态的次數。
    • CPU 總體占用率:線程所使用 CPU 資源占總 CPU 資源的百分比。預設情況下不提取此值。要啟用它,請在表上方選擇 CPU 概要分析。
    • 死鎖:線程是否已死鎖。預設情況下不提取此值。要啟用它,請在表上方選擇死鎖檢測。
    • 已配置設定的位元組:已配置設定給線程的位元組數。預設情況下不提取此值。要啟用它,請在表上方選擇配置設定。
實時監視最後三個值會消耗大量系統資源。這就是預設情況下禁用它們的原因。使用表上方相應的複選框,對這些值啟用監視。
  • 標明線程的堆棧跟蹤

    在活動線程表中選擇線程後,其整個堆棧跟蹤将顯示在下面的標明線程的堆棧跟蹤面闆中。堆棧跟蹤包含所有方法 (一直到目前執行的方法) 的調用路徑。它非常有用,例如,當需要确定導緻死鎖或代碼執行過程中意外中斷的方法時。

可以使用 Ctrl 鍵在活動線程表中選擇多個線程來顯示多個堆棧跟蹤。

3.4.4.8診斷指令

使用診斷指令可監視 Java 應用程式的效率和性能。JMC 使用大量不同的診斷工具,包括一組可以使用診斷指令頁籤針對應用程式運作的指令。

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全
運作 JMX 控制台監視 JVM 的額外成本很小,幾乎可以忽略不計。它提供低成本的應用程式監視和概要分析 JMX标準參考

四. HotSpot虛拟機插件及工具

HotSpot虛拟機發展了二十餘年,現在已經是一套很複雜的軟體系統,如果深入挖掘HotSpot的源碼,可以發現在HotSpot的研發過程中,開發團隊曾經編寫(或者收集)過不少虛拟機的插件和輔助工 具,它們存放在HotSpot源碼hotspot/src/share/tools目錄下,包括(含曾經有過但新版本中已被移除 的):

  • Ideal Graph Visualizer:用于可視化展示C2即時編譯器是如何将位元組碼轉化為理想圖,然後轉化為機器碼的。
  • Client Compiler Visualizer[1]:用于檢視C1即時編譯器生成進階中間表示(HIR),轉換成低級中間表示(LIR)和做實體寄存器配置設定的過程。
  • MakeDeps:幫助處理HotSpot的編譯依賴的工具。
  • Project Creator:幫忙生成Visual Studio的.project檔案的工具。
  • LogCompilation:将-XX:+LogCompilation輸出的日志整理成更容易閱讀的格式的工具。
  • HSDIS:即時編譯器的反彙編插件。

HSDIS:JIT生成代碼反彙編

HSDIS是一個被官方推薦的HotSpot虛拟機即時編譯代碼的反彙編插件,它包含在HotSpot虛拟機 的源碼當中[2],在OpenJDK的網站[3]也可以找到單獨的源碼下載下傳,但并沒有提供編譯後的程式。

HSDIS插件的作用是讓HotSpot的-XX:+PrintAssembly指令調用它來把即時編譯器動态生成的本地代碼還原為彙編代碼輸出,同時還會自動産生大量非常有價值的注釋,這樣我們就可以通過輸出的彙編代碼來從最本質的角度分析問題。

另外還有一點需要注意,如果使用的是SlowDebug或者FastDebug版的HotSpot,那可以直接通 過-XX:+PrintAssembly指令使用的插件;如果使用的是Product版的HotSpot,則還要額外加入一 個-XX:+UnlockDiagnosticVMOptions參數才可以工作。

測試代碼

public class Bar { 
	int a = 1; 
	static int b = 2; 
	public int sum(int c) { 
		return a + b + c; 
	}
	public static void main(String[] args) { 
		new Bar().sum(3); 
	} 
} 
12345678910           

編譯這段代碼,并使用以下指令執行:

java -XX:+PrintAssembly -Xcomp -XX:CompileCommand=dontinline,*Bar.sum -XX:Compile-Command=compileonly,*Bar.sum test.Bar
1           

其中,參數-Xcomp是讓虛拟機以編譯模式執行代碼,這樣不需要執行足夠次數來預熱就能觸發即時編譯。兩個-XX:CompileCommand的意思是讓編譯器不要内聯sum()并且隻編譯sum(),-XX:+PrintAssembly就是輸出反彙編内容。

測試代碼

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

雖然是彙編,但代碼并不多,我們一句一句來閱讀:

1)mov%eax,-0x8000(%esp):檢查棧溢。

2)push%ebp:儲存上一棧幀基址。

3)sub$0x18,%esp:給新幀配置設定空間。

4)mov 0x8(%ecx),%eax:取執行個體變量a,這裡0x8(%ecx)就是ecx+0x8的意思,前面代碼片段“[Constants]”中提示了“this:ecx=‘test/Bar’”,即ecx寄存器中放的就是this對象的位址。偏移0x8是越 過this對象的對象頭,之後就是執行個體變量a的記憶體位置。這次是通路Java堆中的資料。

5)mov$0x3d2fad8,%esi:取test.Bar在方法區的指針。

6)mov 0x68(%esi),%esi:取類變量b,這次是通路方法區中的資料。

7)add%esi,%eax、add%edx,%eax:做2次加法,求a+b+c的值,前面的代碼把a放在eax中,把b 放在esi中,而c在[Constants]中提示了,“parm0:edx=int”,說明c在edx中。

8)add$0x18,%esp:撤銷棧幀。

9)pop%ebp:恢複上一棧幀。

10)test%eax,0x2b0100:輪詢方法傳回處的SafePoint。

11)ret:方法傳回。

在這個例子中測試代碼比較簡單,肉眼直接看日志中的彙編輸出是可行的,但在正式環境中-XX:+PrintAssembly的日志輸出量巨大,且難以和代碼對應起來,這就必須使用工具來輔助了。 JITWatch[5]是HSDIS經常搭配使用的可視化的編譯日志分析工具,為便于在JITWatch中讀取,讀者可使用以下參數把日志輸出到logfile檔案:

-XX:+UnlockDiagnosticVMOptions

-XX:+TraceClassLoading

-XX:+LogCompilation

-XX:LogFile=/tmp/logfile.log

-XX:+PrintAssembly

-XX:+TraceClassLoading

在JITWatch中加載日志後,就可以看到執行期間使用過的各種對象類型和對應調用過的方法了, 界面如圖4-28所示。

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

選擇想要檢視的類和方法,即可檢視對應的Java源代碼、位元組碼和即時編譯器生成的彙編代碼, 如圖4-29所示。

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

繼續閱讀