京東必考JVM 問題診斷快速入門

JVM 問題診斷快速入門

JVM 全稱爲 Java Virtual Machine,翻譯爲中文 “Java 虛擬機”。本文中的JVM主要指 Oracle 公司的 HotSpot VM, 版本是 Java8(JDK8、JDK1.8 是同樣的版本)。如今關於 JVM 的文章、書籍有很多。 有基礎的,也有深入的。

本文主要介紹各種簡單工具的使用,穿插一些基本的知識點。 目的是爲了讓初學者快速上手,先實現入門。入門的意思,按我的理解就是: 會描述問題,知道怎麼去搜索,怎麼去找路子深入學習。

1. 環境準備與相關設置

1.1 安裝JDK,以及設置環境變量.

JDK 通常是從 Oracle官網下載, 打開頁面翻到底部,找 Java for Developers, 下載對應的 x64 版本即可。

現在流行將下載鏈接放到頁面底部,很多工具都這樣。當前推薦下載 JDK8。 今後 JDK11 可能成爲主流版本,因爲 Java11 是 LTS 長期支持版本,但可能還需要一些時間纔會普及,而且兩個版本的結構不太兼容。

有的操作系統提供了自動安裝工具,直接使用也可以,比如 yum, brew, apt 等等。這部分比較基礎,有問題直接搜索即可。

安裝完成後,Java 環境一般來說就可以使用了。 驗證的腳本命令爲:

java -version

如果找不到命令,需要設置環境變量: JAVA_HOMEPATH

JAVA_HOME 環境變量表示 JDK 的安裝目錄,通過修改 JAVA_HOME ,可以快速切換 JDK 版本 。很多工具依賴此環境變量。

另外, 建議不要設置 CLASS_PATH 環境變量,新手沒必要設置,容易造成一些困擾。

Windows系統, 系統屬性 - 高級 - 設置系統環境變量。 如果沒權限也可以設置用戶環境變量。

Linux 和 MacOSX 系統, 需要配置腳本。 例如:

$ cat ~/.bash_profile

\# JAVA ENV
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home
export PATH=$PATH:$JAVA_HOME/bin

讓環境配置立即生效:

$ source ~/.bash_profile

查看環境變量:

echo $PATH
echo $JAVA_HOME

一般來說,.bash_profile 之類的腳本只用於設置環境變量。 不設置隨機器自啓動的程序。

如果不知道自動安裝/別人安裝的JDK在哪個目錄怎麼辦?

最簡單/最麻煩的查詢方式是詢問相關人員。

查找的方式很多,比如,可以使用 whichwhereisls -l 跟蹤軟連接, 或者 find 命令全局查找(可能需要sudo權限), 例如:

jps -v
whereis javac
ls -l /usr/bin/javac
find / -name javac

找到滿足 $JAVA_HOME/bin/javac 的路徑即可。

WIndows系統,安裝在哪就是哪,通過任務管理器也可以查看某個程序的路徑,注意 JAVA_HOME 不可能是 C:/Windows/System32 目錄。

1.2 其他準備工作

按照官方文檔的說法,在診斷問題之前,可以做這些準備:

  • 取消 core 文件限制

    操作系統提供限制可使用資源量的方式,這些限制可能會影響 Java應用程序,如果限制太低,系統可能無法生成完整的 Java 轉儲文件。解除限制方式:ulimit -c unlimited

  • 開啓自動內存Dump選項

    增加啓動參數 -XX:+HeapDumpOnOutOfMemoryError, 在內存溢出時自動轉儲,下文會進行介紹。

  • 可以生成 JFR 記錄

    這是 JDK 內置工具 jmc 提供的功能,Oracle 試圖用 jmc 來取代 JVisualVM。而且在商業環境使用JFR需要付費授權,我認爲這是個雞肋功能,如果想學習也可以試試。

  • 開啓 GC 日誌

    這是標配。比如設置 -verbose:gc 等選項,請參考下文。

  • 確定JVM版本以及啓動參數

    有多種手段獲取, 最簡單的是 java -version,但某些環境下有多個JDK版本,請參考下文。

  • 允許 JMX 監控信息

    JMX支持遠程監控,通過設置屬性來啓用,請參考下文。

2. 常用性能指標介紹

沒有量化就沒有改進,所以我們需要先了解和度量性能指標。

JVM診斷是要診斷些什麼呢?

  1. 程序BUG: 程序問題必須是優先解決的,要保證正確性。例如死鎖等等, 當然,JVM層面的排查只是輔助手段,更多的還是分析業務代碼和邏輯確定Java程序哪裏有問題。
  2. 系統性能問題: 比如藉助監控和日誌,判斷資源層面有沒有問題? JVM層面有沒有問題?

進行JVM問題診斷的目的有:

  • 排查程序運行中出現的問題
  • 優化性能

計算機系統中,性能相關的資源主要分爲這幾類:

  • CPU
  • 內存
  • IO: 存儲+網絡

性能優化中常見的套路:

一般先排查基礎資源是否成爲瓶頸。看資源夠不夠,只要成本允許,加配置可能是最快速的解決方案,還可能是最划算,最有效的解決方案。

與JVM有關的系統資源,主要是 CPU內存 這兩部分。 如果發生資源告警/不足, 就需要評估系統容量,分析原因。

至於 GPU 、主板、芯片組之類的資源則不太好衡量,通用計算系統很少涉及。

一般衡量系統性能的維度有3個:

  • 延遲(Latency): 一般衡量的是響應時間(Response Time),比如95線,99線,最大響應時間等。
  • 吞吐量(Throughput): 一般的衡量每秒處理的事務數(TPS)。
  • 系統容量(Capacity): 也叫做設計容量,可以理解爲硬件配置,成本約束。

這3個維度互相關聯,相互制約。只要系統架構允許,增加硬件配置一般都能提升性能指標。

性能指標還可分爲兩類:

  • 業務需求指標:如吞吐量(QPS、TPS)、響應時間(RT)、併發數、業務成功率等。
  • 資源約束指標:如CPU、內存、I/O等資源的消耗情況。

詳情可參考: 性能測試中服務器關鍵性能指標淺析

每類系統關注的重點還不一樣。 批處理/流處理 系統更關注吞吐量, 延遲可以適當放寬。一般來說大部分系統的硬件資源不會太差,但也不是無限的。高可用Web系統,既關注高併發情況下的系統響應時間,也關注吞吐量。

例如: “配置2核4GB的節點,每秒響應200個請求,95%線是20ms,最大響應時間40ms。” 從中可以解讀出基本的性能信息: 響應時間(RS<40ms), 吞吐量(200TPS), 系統配置信息(2C4G)。 隱含的條件可能是 "併發請求數不超過200 "。

我們可採用的方式:

  • 本地/遠程調試, 下文將簡單介紹JDWP與遠程調試
  • 狀態監控
  • 性能分析: CPU使用情況/內存分配分析
  • 內存分析: Dump分析/GC日誌分析
  • JVM啓動參數調整

怎樣纔算入門?

  • 掌握基本的JVM基礎知識, 內存劃分, 常用參數配置
  • 會使用工具, 看懂日誌,監控, 判斷各種指標是否正常
  • 會搜索問題,能大致確定是什麼問題,搜索/諮詢解決方案, 或者自己搞定

不入門就是"門外漢",門外漢就是隻知道這裏有一個龐然大物,雲裏霧裏,不瞭解裏面有什麼, 需要注意些什麼。

只要工具用好了,獲取到相關的狀態信息,並能簡單分析,那麼JVM診斷這個技能就算是入門了,

本文從如何各類易於上手的工具作爲切入點,講解如何對JVM進行問題診斷, 通過監控和分析,判斷是不是JVM層面的問題。

實際上,用好 JVisulVM和相關的各種配置,JVM診斷就算是入門了。

3. JVM基礎知識和啓動參數

Java體系中有很多規範, 其中最基本的是 Java語言規範Java虛擬機規範

各個領域的神級人物,一般都通讀和掌握相關規範。

3.1 JVM背景知識

JVM是Java程序的底層執行環境,主要用C++語言開發,如果想深入探索JVM,那麼就需要掌握一定的C++語言知識,至少也應該看得懂C++代碼。

JVM的操作對象是class文件,而不是java源碼。

看起來很難的樣子。 就像很多神書一樣,講JVM開篇就講怎麼編譯JVM。講JMM一來就引入CPU寄存器怎麼同步。

當然,不需要很深的技術棧,也是可以排查JVM問題的。

題外話:目前,最多的Java虛擬機實例位於 Android 設備中,絕大部分的Linux系統也運行在 Android 設備上。

在前些年,由於存儲的限制,軟件安裝包的大小很受關注。Java安裝包分爲 2 種類型: JDK 是完全版安裝包, JRE是閹割版安裝包。

如今Java語言的主要應用領域是企業環境,所以 Oracle 在 JRE 的基礎上,增加一部分 JDK 內置的工具,組合成 Server JRE 版,但實際上這增加了選擇的複雜度,有點雞肋。實際使用時直接安裝 JDK 即可。

先來看看需要具備哪些基礎知識,有相關基礎的讀者大致過一眼即可。

3.2 JVM 內存結構

根據對 JVM 內存劃分的理解,製作了幾張邏輯概念圖。大家可以參考一下。

先看看棧內存(Stack)的大體結構:

每啓動一個線程,JVM 就會在棧空間棧分配對應的 線程棧, 比如 1MB 的空間(-Xss1m)。

線程棧也叫做 Java 方法棧。 如果使用了 JNI 方法,則會分配一個單獨的本地方法棧(Native Stack).

線程執行過程中,一般會有多個方法組成調用棧(Stack Trace), 比如A調用B,B調用C。。。每執行到一個方法,就會創建對應的 棧幀(Frame).

比如返回值需要有一個空間存放吧,每個局部變量都需要對應的地址空間,此外還有操作數棧,以及方法指針(標識這個棧幀對應的是哪個類的哪個方法,指向常量池中的字符串常量)。

Java 程序除了棧內存之外,最主要內存區域就是堆內存了。

堆內存是所有線程共用的內存空間,理論上大家都可以訪問裏面的內容。

GC 理論中有一個重要的思想,叫做分代。 經過研究發現,程序中分配的對象,要麼用過就扔,要麼就能存活很久很久。

JVM 將 Heap 內存分爲年輕代(Young generation)和老年代(Old generation,也叫 Tenured)兩部分。

年輕代還劃分爲3個內存池,新生代(Eden space)和存活區(Survivor space), 存活區在大部分 GC 算法中有2個(S0, S1),S0和S1總有一個是空的,但一般較小,也不浪費多少空間。

具體實現對新生代還有優化,那就是 TLAB(Thread Local Allocation Buffer), 給每個線程先劃定一小片空間,你創建的對象先在這裏分配,滿了再換。這能極大降低併發資源鎖定的開銷。

Non-Heap 本質上還是 Heap,只是一般不歸 GC 管理,裏面劃分爲3個內存池。

  • Metaspace, 以前叫持久代(永久代,Permanent generation), Java8 換了個名字叫 Metaspace. Java8 將方法區移動到了Meta區裏面,而方法又是class的一部分。。。和CCS交叉了?
  • CCS,Compressed Class Space, 存放 class 信息的,和 Metaspace 有交叉。
  • Code Cache,存放 JIT 編譯器編譯後的本地機器代碼。

JVM 的內存結構大致如此。

還可以參考 Metaspace 解密

3.3 JVM 啓動參數

啓動 Java 程序的格式爲:

java [options] classname [args]

java [options] -jar filename [args]

其中:

  • [options] 部分稱爲 “JVM 選項”,對應 IDE 中的 VM options, 可用 jps -v 查看。
  • [args] 部分是指 “傳給 main 函數的參數”, 對應IDE中的 Program arguments, 可用 jps -m 查看。

Java 和 JDK 內置的工具,指定參數時都是一個 -,不管是長參數還是短參數。

有時候,JVM 啓動參數和Java程序啓動參數,並沒必要嚴格區分,大致知道都是一個概念即可。

JVM 的啓動參數, 從形式上可以簡單分爲:

-??? 標準選項,java 中各種參數都是一個橫線 - 開頭, 很少有兩個橫線 --; -X 非標準選項, 基本都是傳給JVM的。 -XX: 高級擴展選項, 專門用於控制JVM的行爲。

  • -XX:+-Flags 形式, +- 是對布爾值進行開關。
  • -XX:key=value 形式, 指定某個選項的值。

3.3.1 設置系統屬性

使用 -Dproperty=value 這種形式。

例如指定隨機數熵源(Entropy Source), 示例:

JAVA_OPTS="-Djava.security.egd=file:/dev/./urandom"

3.3.2 agent 相關的選項:

agent是JVM中的一項黑科技, 可以通過無侵入方式來做很多事情,比如注入AOP代碼,執行統計等等,權限非常大。

設置 agent 的語法如下:

  • -agentlib:libname[=options] 啓用native方式的agent, 參考 LD_LIBRARY_PATH 路徑。
  • -agentpath:pathname[=options] 啓用native方式的agent
  • -javaagent:jarpath[=options] 啓用外部的agent庫, 比如 pinpoint.jar 等等。
  • -Xnoagent 則是禁用所有 agent。

示例, 開啓CPU使用時間抽樣分析:

JAVA_OPTS="-agentlib:hprof=cpu=samples,file=cpu.samples.log"

hprof是JDK內置的一個性能分析器。cpu=samples 會抽樣在各個方法消耗的時間佔比, Java進程退出後會將分析結果輸出到文件。

3.3.3 JVM運行模式:

-server 指定服務器模式, 64位 JDK 只支持該選項,是否設置都是這個值。

JDK1.7 之前 x86.32 位的默認值是 -client 選項, 主要原因是以前 JIT 編譯器佔內存,可能還有點慢。

示例:

JAVA_OPTS="-server"

3.3.4 設置堆內存

JVM總內存=堆+棧+非堆+堆外內存。。。

參數:

  • -Xmx, 指定最大堆內存。 如 -Xmx4g. 這只是指定了 Heap 部分的最大值爲4g。
  • -Xms, 指定堆內存空間的起始值。 如 -Xms4g。 並不是操作系統實際分配的初始值,而是 GC 先規劃好,用到才分配。 專用服務器上讓 -Xms-Xmx一致,GC 日誌會比較好看,不然剛啓動可能就有好幾個FullGCC。 據說不一致時,堆內存擴容會有性能抖動。
  • -Xmn, 等價於 -XX:NewSize, 使用 G1 垃圾收集器 不應該 設置該選項,在某些業務場景下可以設置。官方建議設置爲 -Xmx1/2 ~ 1/4.
  • -XX:MaxPermSize=size, 這是 JDK1.7 之前使用的。Java8默認允許的Meta空間無限大。
  • -XX:MaxMetaspaceSize=size, Java8 默認不限制 Meta 空間, 一般不允許設置該選項。

示例:

JAVA_OPTS="-Xms28g -Xmx28g"

3.3.5 設置棧內存

  • -Xss,設置每個線程棧的字節數。 例如 -Xss1m 指定線程棧爲 1MB。
  • -XX:ThreadStackSize=1m, 和 -Xss1m 等價

示例:

JAVA_OPTS="-Xss1m"

3.3.6 GC 日誌相關

  • -verbose:gc 參數

和其他 GC 參數組合使用, 在 GC 日誌中輸出詳細的 GC 信息。 包括每次 GC 前後各個內存池的大小,堆內存的大小,提升到老年代的大小,以及消耗的時間。

此參數支持在運行過程中動態開關。比如使用 jcmd, jinfo, 以及使用JMX技術的其他客戶端。

  • -XX:+PrintGCDetails-XX:+PrintGCTimeStamps, 打印 GC 細節與發生時間。參考 GC 部分。

示例:

export JAVA_OPTS="-Xms28g -Xmx28g -Xss1m \
-verbosegc -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/"

3.3.7 指定垃圾收集器

指定具體的垃圾收集器。

  • -XX:+UseG1GC
  • -XX:+UseConcMarkSweepGC
  • -XX:+UseSerialGC
  • -XX:+UseParallelGC

3.3.8 特殊情況執行腳本

  • -XX:+-HeapDumpOnOutOfMemoryError 選項, 當 OutOfMemoryError 產生,即內存溢出(堆內存或持久代)時,自動Dump堆內存。 因爲在運行時並沒有什麼開銷, 所以在生產機器上是可以使用的。 示例用法: java -XX:+HeapDumpOnOutOfMemoryError -Xmx256m ConsumeHeap
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java\_pid2262.hprof ...
......

  • -XX:HeapDumpPath 選項, 與HeapDumpOnOutOfMemoryError搭配使用, 指定內存溢出時 Dump 文件的目錄。 如果沒有指定則默認爲啓動 Java 程序的工作目錄。 示例用法: java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/ ConsumeHeap 自動 Dump 的 hprof 文件會存儲到 /usr/local/ 目錄下。
  • -XX:OnError 選項, 發生致命錯誤時(fatal error)執行的腳本。 例如, 寫一個腳本來記錄出錯時間, 執行一些命令, 或者 curl 一下某個在線報警的 url. 示例用法: java -XX:OnError="gdb - %p" MyApp 可以發現有一個 %p 的格式化字符串,表示進程PID。
  • -XX:OnOutOfMemoryError 選項, 拋出 OutOfMemoryError 錯誤時執行的腳本。
  • -XX:ErrorFile=filename 選項, 致命錯誤的日誌文件名,絕對路徑或者相對路徑。

4. JDK 內置工具介紹和使用示例

本節介紹 JDK 內置的各種工具, 包括

  • 命令行工具
  • GUI 工具
  • 服務端工具

很多情況下,JVM 運行環境中並沒有趁手的工具, 這時候可以先用命令行工具快速查看JVM實例的基本情況。

MacOSX,Windows 系統的某些賬戶權限不夠,有些工具可能會報錯/失敗,假如出問題了請排除這個因素。

下面先介紹命令行工具。

4.1 jps 工具簡介

我們知道,操作系統提供一個工具叫做 ps, 用於顯示進程狀態(process status)。

Java也提供了類似的命令行工具,叫做 jps, 用於展示java進程信息(列表)。

需要注意的是, jps展示的是當前用戶可看見的 Java 進程,如果看不見可能需要 sudosu 之類的命令輔助。

查看幫助信息:

$ jps -help

usage: jps [-help]
       jps [-q] [-mlvV] [<hostid>]
Definitions:
    <hostid>:      <hostname>[:<port>]

可以看到, 這些參數分爲了多個組, -help-q-mlvV, 同一組可以共用一個 -

常用的參數是小寫的 -v, 顯示傳遞給JVM的啓動參數.

$ jps -v

15883 Jps -Dapplication.home=/usr/local/jdk1.8.0_74 -Xms8m
6446 Jstatd -Dapplication.home=/usr/local/jdk1.8.0_74 -Xms8m
        -Djava.security.policy=/etc/java/jstatd.all.policy
32383 Bootstrap -Xmx4096m -XX:+UseG1GC -verbose:gc
        -XX:+PrintGCDateStamps -XX:+PrintGCDetails 
        -Xloggc:/xxx-tomcat/logs/gc.log
        -Dcatalina.base=/xxx-tomcat -Dcatalina.home=/data/tomcat

看看輸出的內容,其中最重要的信息是前面的進程ID(PID),

其他參數不太常用:

  • -q 只顯示進程號。

  • -m 顯示傳給 main 方法的參數信息

  • -l 顯示啓動 class 的完整類名, 或者啓動 jar 的完整路徑

  • -V 大寫的V,這個參數有問題, 相當於沒傳一樣。官方說的跟 -q 差不多。

  • <hostid> 部分是遠程主機的標識符,需要遠程主機啓動 jstatd 服務器支持。

    可以看到, 格式爲 <hostname>[:<port>], 不能用IP, 示例: jps -v sample.com:1099

知道JVM進程的PID之後,就可以使用其他工具來進行診斷了。

4.2 jstat 工具簡介

jstat 用來監控JVM內置的各種統計信息,主要是內存和GC相關的信息。

查看 jstat 的幫助信息, 大致如下:

$ jstat -help

Usage: jstat -help|-options
       jstat -\<option\> [-t] [-h\<lines\>] \<vmid\> [\<interval\> [\<count\>]]

Definitions:
  \<option\>      可用的選項, 查看詳情請使用 -options
  \<vmid\>        虛擬機標識符. 格式: \<lvmid\>[@\<hostname\>[:\<port\>]]
  \<lines\>       標題行間隔的頻率.
  \<interval\>    採樣週期, \<n\>["ms"|"s"], 默認單位是毫秒 "ms".
  \<count\>       採用總次數.
  -J\<flag\>      傳給jstat底層JVM的 \<flag\> 參數.

再來看看 <option> 部分支持哪些選項:

$ jstat -options

-class
-compiler
-gc
-gccapacity
-gccause
-gcmetacapacity
-gcnew
-gcnewcapacity
-gcold
-gcoldcapacity
-gcutil
-printcompilation

簡單說明這些選項, 不感興趣可以跳着讀。

  • -class 類加載(Class loader)信息統計.
  • -compiler JIT即時編譯器相關的統計信息。
  • -gc GC相關的堆內存信息. 用法: jstat -gc -h 10 -t 864 1s 20
  • -gccapacity 各個內存池分代空間的容量。
  • -gccause 看上次GC, 本次GC(如果正在GC中)的原因, 其他輸出和 -gcutil 選項一致。
  • -gcnew 年輕代的統計信息. (New = Young = Eden + S0 + S1)
  • -gcnewcapacity 年輕代空間大小統計.
  • -gcold 老年代和元數據區的行爲統計。
  • -gcoldcapacity old空間大小統計.
  • -gcmetacapacity meta區大小統計.
  • -gcutil GC相關區域的使用率(utilization)統計。
  • -printcompilation 打印JVM編譯統計信息。

實例:

jstat -gcutil -t 864

-gcutil 選項是統計GC相關區域的使用率(utilization), 結果如下:

Timestamp S0 S1 E O M CCS YGC YGCT FGC FGCT GCT     14251645.5 0.00 13.50 55.05 71.91 83.84 69.52 113767 206.036 4 0.122 206.158    

-t 選項的位置是固定的,不能在前也不能在後。 可以看出是用於顯示時間戳,即 JVM 啓動到現在的秒數。

簡單分析一下:

  • Timestamp 列:JVM 啓動了1425萬秒,大約164天。
  • S0 就是 0 號存活區的百分比使用率。 0% 很正常, 因爲 S0 和 S1 隨時有一個是空的。
  • S1 就是1號存活區的百分比使用率。
  • E 就是 Eden 區,新生代的百分比使用率。
  • O 就是 Old 區, 老年代。百分比使用率。
  • M 就是 Meta 區, 元數據區百分比使用率。
  • CCS, 壓縮 class 空間(Compressed class space)的百分比使用率。
  • YGC (Young GC), 年輕代 GC 的次數。11 萬多次, 不算少。
  • YGCT 年輕代 GC 消耗的總時間。206 秒, 佔總運行時間的萬分之一不到,基本上可忽略。
  • FGC FullGC 的次數,可以看到只發生了4次,問題應該不大。
  • FGCT FullGC 的總時間, 0.122秒,平均每次30ms左右,大部分系統應該能承受。
  • GCT 所有GC加起來消耗的總時間, 即YGCT+FGCT

可以看到, -gcutil 這個選項出來的信息不太好用, 統計的結果是百分比,不太直觀。

再看看, -gc 選項, GC相關的堆內存信息.

jstat -gc -t 864 1s
jstat -gc -t 864 1s 3
jstat -gc -t -h 10 864 1s 15

其中的 1s 佔了 <interval> 這個槽位, 表示每1秒輸出一次信息。

1s 3 的意思是每秒輸出1次,最多3次。

如果只指定刷新週期, 不指定 <count> 部分, 則會一直持續輸出。 退出輸出按 CTRL+C即可。

-h 10 的意思是每10行輸出一次表頭。

結果大致如下:

Timestamp S0C S1C S0U S1U EC EU OC OU MC MU YGC YGCT FGC FGCT     14254245.3 1152.0 1152.0 145.6 0.0 9600.0 2312.8 11848.0 8527.3 31616.0 26528.6 113788 206.082 4 0.122   14254246.3 1152.0 1152.0 145.6 0.0 9600.0 2313.1 11848.0 8527.3 31616.0 26528.6 113788 206.082 4 0.122   14254247.3 1152.0 1152.0 145.6 0.0 9600.0 2313.4 11848.0 8527.3 31616.0 26528.6 113788 206.082 4 0.122    

上面的結果是精簡過的, 爲了排版去掉了 GCTCCSCCCSU 這三列。 看到這些單詞可以試着猜一下意思, 詳細的解讀如下:

  • Timestamp 列: JVM 啓動了1425萬秒,大約164天。
  • S0C: 0 號存活區的當前容量(capacity), 單位 kB.
  • S1C: 1 號存活區的當前容量, 單位 kB.
  • S0U: 0 號存活區的使用量(utilization), 單位 kB.
  • S1U: 1 號存活區的使用量, 單位 kB.
  • EC: Eden 區,新生代的當前容量, 單位 kB.
  • EU: Eden 區,新生代的使用量, 單位 kB.
  • OC: Old 區, 老年代的當前容量, 單位 kB.
  • OU: Old 區, 老年代的使用量, 單位 kB. (!需要關注)
  • MC: 元數據區的容量, 單位 kB.
  • MU: 元數據區的使用量, 單位 kB.
  • CCSC: 壓縮的 class 空間容量, 單位 kB.
  • CCSU: 壓縮的 class 空間使用量, 單位 kB.
  • YGC: 年輕代 GC 的次數。
  • YGCT: 年輕代 GC 消耗的總時間。 (!重點關注)
  • FGC: Full GC 的次數
  • FGCT: Full GC 消耗的時間. (!重點關注)
  • GCT: 垃圾收集消耗的總時間。

最重要的信息是 GC 的次數和總消耗時間,其次是老年代的使用量。

在沒有其他監控工具的情況下, jstat 可以簡單查看各個內存池和GC的信息,可用於判別是否是GC問題或者內存溢出。

4.3 jmap 工具

面試最常問的就是 jmap 工具了。

jmap 主要用來 dump 堆內存。當然也支持輸出統計信息。

官方推薦使用 JDK8 自帶的 jcmd 工具來取代 jmap, 但是 jmap 深入人心,jcmd 可能暫時取代不了。

查看 jmap 幫助信息:

$ jmap -help

Usage:
    jmap [option] <pid>
        (連接到本地進程)
    jmap [option] <executable <core>
        (連接到 core file)
    jmap [option] [server_id@]<remote-IP-hostname>
        (連接到遠程 debug 服務)

where <option> is one of:
    <none>               等同於 Solaris 的 pmap 命令
    -heap                打印Java堆內存彙總信息
    -histo[:live]        打印Java堆內存對象的直方圖統計信息;
                         如果指定了 "live" 選項則只統計存活對象,強制觸發一次GC
    -clstats             打印class loader 統計信息
    -finalizerinfo       打印等待 finalization 的對象信息
    -dump:<dump-options> 將堆內存dump爲 hprof 二進制格式
                         支持的 dump-options:
                           live         只dump存活對象; 不指定則導出全部.
                           format=b     二進制格式(binary format)
                           file=<file>  導出文件的路徑
                         示例: jmap -dump:live,format=b,file=heap.bin <pid>
    -F                   強制導出. 若jmap被hang住不響應, 可斷開後使用此選項。
                         其中 "live" 選項不支持強制導出.
    -h | -help           to print this help message
    -J<flag>             to pass <flag> directly to the runtime system

常用選項就3個:

  • -heap 打印堆內存(/內存池)的配置和使用信息。
  • -histo 看哪些類佔用的空間最多, 直方圖
  • -dump:format=b,file=xxxx.hprof Dump堆內存。

示例:

看堆內存統計信息。

$ jmap -heap 4524

輸出信息:

Attaching to process ID 4524, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.65-b01

using thread-local object allocation.
Parallel GC with 4 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 2069889024 (1974.0MB)
   NewSize                  = 42991616 (41.0MB)
   MaxNewSize               = 689963008 (658.0MB)
   OldSize                  = 87031808 (83.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 24117248 (23.0MB)
   used     = 11005760 (10.49591064453125MB)
   free     = 13111488 (12.50408935546875MB)
   45.63439410665761% used
From Space:
   capacity = 1048576 (1.0MB)
   used     = 65536 (0.0625MB)
   free     = 983040 (0.9375MB)
   6.25% used
To Space:
   capacity = 1048576 (1.0MB)
   used     = 0 (0.0MB)
   free     = 1048576 (1.0MB)
   0.0% used
PS Old Generation
   capacity = 87031808 (83.0MB)
   used     = 22912000 (21.8505859375MB)
   free     = 64119808 (61.1494140625MB)
   26.32600715361446% used

12800 interned Strings occupying 1800664 bytes.

可以看到堆內存和內存池的相關信息。

當然,這些信息有多種方式可以得到,比如 JMX。

看看直方圖

$ jmap -histo 4524

結果爲:

 num     \#instances \#bytes class name
----------------------------------------------
   1:         52214       11236072  [C
   2:        126872        5074880  java.util.TreeMap$Entry
   3:          5102        5041568  [B
   4:         17354        2310576  [I
   5:         45258        1086192  java.lang.String
......

簡單分析, 其中 [C 佔用了11MB內存,沒佔用什麼空間。

[C 表示 chat[], [B 表示 byte[], [I 表示 int[], 其他類似。這種基礎數據類型很難分析出什麼問題。

Java中的大對象, 巨無霸對象,一般都是長度很大的數組。

Dump堆內存:

cd $CATALINA_BASE
jmap -dump:format=b,file=3826.hprof 3826

導出完成後, dump文件大約和堆內存一樣大。 可以想辦法壓縮並傳輸。

分析 hprof 文件可以使用 jhat 或者 mat 工具。

4.4 jcmd工具

診斷工具

jcmd 是 JDK8 推出的一款本地診斷工具,只支持連接本機上同一個用戶空間下的 JVM 進程。

查看幫助:

$ jcmd -help

Usage: jcmd <pid | main class> <command ...|PerfCounter.print|-f file>
   or: jcmd -l                                                    
   or: jcmd -h                                                    

  command 必須是指定JVM可用的有效 jcmd 命令.      
  可以使用 "help" 命令查看該 JVM 支持哪些命令.   
  如果指定 pid 部分的值爲 0, 則會將 commands 發送給所有可見的 Java 進程.   
  指定 main class 則用來匹配啓動類。可以部分匹配。(適用同一個類啓動多實例).                         
  If no options are given, lists Java processes (same as -p).     

  PerfCounter.print 命令可以展示該進程暴露的各種計數器
  -f  從文件讀取可執行命令                  
  -l  列出(list)本機上可見的 JVM 進程                     
  -h  this help                           

查看進程信息:

jcmd
jcmd -l
jps -lm

11155 org.jetbrains.idea.maven.server.RemoteMavenServer

這幾個命令的結果差不多。可以看到其中有一個 PID 爲 11155 的進程。

下面看看可以用這個 PID 做什麼。

給這個進程發一個 help 指令:

jcmd 11155 help
jcmd RemoteMavenServer help

pid 和 main-class 輸出信息是一樣的:

11155:
The following commands are available:
VM.native_memory
ManagementAgent.stop
ManagementAgent.start_local
ManagementAgent.start
GC.rotate_log
Thread.print
GC.class_stats
GC.class_histogram
GC.heap_dump
GC.run_finalization
GC.run
VM.uptime
VM.flags
VM.system_properties
VM.command_line
VM.version
help

可以試試這些命令。查看VM相關的信息 :

\# JVM實例運行時間
jcmd 11155 VM.uptime
9307.052 s
 \#JVM 版本號
jcmd 11155 VM.version
OpenJDK 64-Bit Server VM version 25.76-b162
JDK 8.0_76
 \# JVM 實際生效的配置參數
jcmd 11155 VM.flags
11155:
-XX:CICompilerCount=4 -XX:InitialHeapSize=268435456
-XX:MaxHeapSize=536870912 -XX:MaxNewSize=178782208
-XX:MinHeapDeltaBytes=524288 -XX:NewSize=89128960
-XX:OldSize=179306496 -XX:+UseCompressedClassPointers
-XX:+UseCompressedOops -XX:+UseParallelGC
 \# 查看命令行參數
jcmd 11155 VM.command_line
VM Arguments:
jvm_args: -Xmx512m -Dfile.encoding=UTF-8
java_command: org.jetbrains.idea.maven.server.RemoteMavenServer
java_class_path (initial): ...(xxx省略)...
Launcher Type: SUN_STANDARD
 \# 系統屬性
jcmd 11155 VM.system_properties
...
java.runtime.name=OpenJDK Runtime Environment
java.vm.version=25.76-b162
java.vm.vendor=Oracle Corporation
user.country=CN

GC 相關的命令,

統計每個類的實例佔用字節數。

$ jcmd 11155 GC.class_histogram

 num     #instances         #bytes  class name
----------------------------------------------
   1:         11613        1420944  [C
   2:          3224         356840  java.lang.Class
   3:           797         300360  [B
   4:         11555         277320  java.lang.String
   5:          1551         193872  [I
   6:          2252         149424  [Ljava.lang.Object;

Dump堆內存:

$jcmd 11155 help GC.heap_dump

Syntax : GC.heap_dump [options] <filename>
Arguments: filename :  Name of the dump file (STRING, no default value)
Options:  -all=true 或者 -all=false (默認)
 \# 兩者效果差不多; jcmd 需要指定絕對路徑; jmap不能指定絕對路徑
jcmd 11155 GC.heap_dump -all=true ~/11155-by-jcmd.hprof
jmap -dump:file=./11155-by-jmap.hprof 11155

jcmd 坑的地方在於, 必須指定絕對路徑, 否則導出的 hprof 文件就以 JVM 所在的目錄計算。(: 因爲是發命令交給 jvm 執行的。)

其他命令用法類似,必要時請參考官方文檔。

4.5 jstack 工具

命令行工具、診斷工具

jstack 工具可以打印出 Java 線程的調用棧信息(stack trace)。

一般用來查看存在哪些線程,診斷是否存在死鎖等。

這時候就看出來給線程(池)命名的必要性了,【開發不規範,整個項目都是坑】,具體可參考阿里巴巴的 Java 開發規範。

看看幫助信息:

$jstack -help

Usage:
    jstack [-l] <pid>
        (to connect to running process)
    jstack -F [-m] [-l] <pid>
        (to connect to a hung process)
    jstack [-m] [-l] <executable> <core>
        (to connect to a core file)
    jstack [-m] [-l] [server_id@]<remote server IP or hostname>
        (to connect to a remote debug server)

Options:
    -F  to force a thread dump. Use when jstack <pid> does not respond (process is hung)
    -m  to print both java and native frames (mixed mode)
    -l  long listing. Prints additional information about locks
    -h or -help to print this help message

選項說明:

  • -F 強制執行 thread dump. 可在 Java 進程卡死(hung 住)時使用, 此選項可能需要系統權限。
  • -m 混合模式(mixed mode),將 Java幀和 native 幀一起輸出, 此選項可能需要系統權限。
  • -l 長列表模式,將線程相關的 locks 信息一起輸出,比如持有的鎖,等待的鎖。

常用的選項是 -l, 示例用法。

jstack 4524
jstack -l 4524

死鎖的原因一般是鎖定多個資源的順序出了問題【交叉依賴】, 網上示例代碼很多,比如搜索 Java 死鎖 示例

4.6 jinfo 工具

診斷工具

jinfo 用來查看具體生效的配置信息,以及系統屬性。 還支持動態增加一部分參數。

看看幫助信息:

$ jinfo -help

Usage:
    jinfo [option] <pid>
        (to connect to running process)
    jinfo [option] <executable <core>
        (to connect to a core file)
    jinfo [option] [server_id@]<remote-IP-hostname>
        (to connect to remote debug server)

where <option> is one of:
    -flag <name>         to print the value of the named VM flag
    -flag [+|-]<name>    to enable or disable the named VM flag
    -flag <name>=<value> to set the named VM flag to the given value
    -flags               to print VM flags
    -sysprops            to print Java system properties
    <no option>          to print both of the above
    -h | -help           to print this help message

使用示例:

jinfo 4524
jinfo -flags 4524

不加參數過濾,則打印所有信息。

jinfo在Windows上比較穩定。

筆者在Mac和Linux系統上使用一直報錯,在MacOSX系統上彈出安全警告而被攔截,在Linux上可能是jinfo 版本和目標JVM版本不一致的原因。

Error attaching to process:
  sun.jvm.hotspot.runtime.VMVersionMismatchException:
    Supported versions are 25.74-b02. Target VM is 25.66-b17

而這些性能診斷工具官方並不提供技術支持,所以如果碰到報錯信息,請不要着急,可以試試其他工具。不行就換 JDK 版本。

4.7 jvisualvm 圖形界面監控工具

GUI 圖形界面工具, 主要是3款: jconsole, jvisualvm, jmc。

其中, jconsole 比較古老,在此不進行介紹。

JVisualVM 啓動後的界面大致如下:

在其中可以看到本地的 JVM 實例。

通過 JVisualVM 連接到某個 JVM 以後, "概述"頁籤顯示的基本信息如下圖所示:

<

可以看到,其中有 PID,啓動參數,系統屬性等信息。

切換到"監視"頁籤:

font-smoothing:antialiased;box-sizing:border-box;margin:0px 0px 1.1em;outline:0px;">"線程"頁籤則展示了JVM中的線程列表。 再一次看出在程序中對線程(池)命名的好處。

JVisualVM 強大的功能在於插件。

JVisualVM安裝MBeans插件的步驟: 通過 工具(T) – 插件(G) – 可用插件 – 勾選具體的插件 – 安裝 – 下一步 – 等待安裝完成。

常用的插件是 VisualGC 和 MBeans。

如果看不到可用插件,請安裝最新版本,或者下載插件到本地安裝。

請排除網絡問題,或者檢查更新,重新啓動試試。

切換到 VisualGC 頁籤:

一般不怎麼關注 MBean ,但 MBean 對於理解 GC 的原理倒是挺有用的。

主要看 從圖中可以看到 Metaspace 內存池的 Type 是NON_HEAP


當然,還可以看垃圾收集器(GarbageCollector)。

對所有的垃圾收集器, 通過 JMX API 獲取的信息包括:\<[](resources/):0px;"\>**CollectionCount** : 垃圾收集器執行的 GC 總次數, 

* **CollectionTime**: 收集器運行時間的累計。這個值等於所有 GC 事件持續時間的總和,

* **MemoryPoolNames**: 各個內存池的名稱,

* **Name**: 垃圾收集器的名稱

* **ObjectName**: 由 JMX 規範定義的 MBean 的名字,,

* **Valid**: 此收集器是否有效。本人只見過 "`true`"的情況 (^\_^)

根據經驗, 這些信息對分析 GC 性能來說,不能得出什麼結論. 只有編寫程序, 獲取 GC 相關的 JMX 信息來進行統計和分析。 

下面看怎麼執行遠程實時監控。

如上圖所示,從文件菜單中, 我們可以選擇“添加遠程主機”,以及“添加JMX連接”。

比如 “添加 JMX 連接”, 填上 IP 和端口號之後,勾選“不要求SSL連接”,點擊“確定”按鈕即可。

關於目標 JVM 怎麼啓動JMX支持,請參考下面的 JMX 小節。

遠程主機則需要 jstatd 的支持。請參考 jstatd 部分。

### 4.8 `jmc` 圖形界面客戶端

jmc 和 jvisualvm 功能類似。

Oracle 試圖用 jmc 來取代 JVisualVM,但 jmc 和 jinfo 一樣,都需要比較高的權限(去操縱其他JVM進程),可能會被操作系統的安全限制攔截。在商業環境使用JFR需要付費獲取授權。

啓動後的界面如下:

jmc 界面確實漂亮了很多,有些客戶肯定喜歡。

但 jmc 不只是一個監控工具,要求的權限很多,有些權限不夠的管理員賬戶可能出一些問題。

4.9 jstatd服務端工具 

jstatd 是一款強大的服務端支持工具。

但因爲涉及暴露一些服務器信息,所以需要配置安全策略文件。

> $ `cat /etc/java/jstatd.all.policy`

grant codebase "file:後臺啓動 jstatd的命令:

jstatd -J-Djava.security.policy=jstatd.all.policy
  -J-Djava.rmi.server.hostname=198.11.188.188 &

其中 198.11.188.188 是公網 IP,如果沒有公網,那麼就是內網 IP。

然後使用 jvisualvm, 或者 jconsole 連接遠程服務器。 其中IP爲 198.11.188.188,端口號是默認的 1099. 當然,端口號可以通過參數自定義。

說明: 客戶端與服務器的 JVM 大版本號必須一致或者兼容。

CPU 圖形沒有顯示 ,原因是 jstatd 不監控單個實例的 CPU。 可以啓用對應 JVM 的 JMX 監控, 具體請參考 JMX 一節。

4.10 更多工具

JDK還自帶了其他工具, 比如 jsadebugd 可以在服務端主機上,開啓RMI Server。 jhat 可用於解析hprof內存Dump文件等。

在此不進行介紹,有興趣可以搜索看看。

5. JDWP 簡介

Java 平臺調試體系(Java Platform Debugger Architecture,JPDA),由三個相對獨立的層次共同組成。這三個層次由低到高分別是 Java 虛擬機工具接口(JVMTI),Java 調試線協議(JDWP)以及 Java 調試接口(JDI)。

詳細介紹請參考或搜索: JPDA 體系概覽

JDWP 是 Java Debug Wire Protocol 的縮寫,翻譯爲 “Java調試線協議”,它定義了調試器(debugger)和被調試的 Java 虛擬機(target vm)之間的通信協議。

5.1 服務端 JVM 配置

本節主要講解如何在 JVM 中啓用 JDWP,以供遠程調試。

假設主啓動類是 com.xxx.Test

在 Windows 機器上:

java -Xdebug -Xrunjdwp:transport=dt_shmem,address=debug,server=y,suspend=y com.xxx.Test

在Solaris 或 Linux操作系統上:

java -Xdebug -Xrunjdwp:transport=dt_socket,address=8888,server=y,suspend=y com.xxx.Test

其實, -Xdebug 這個選項什麼用都沒有,官方說是爲了歷史兼容性, 避免報錯纔沒有刪除。

通過這些啓動參數, Test 類將運行在調試模式下, 並等待調試器連接到JVM的調試地址: 在 Windows 上是 debug,在 Oracle Solaris 或 Linux 操作系統上是 8888 端口。

如果細心觀察的話, 會發現 Idea 中 Debug 模式啓動的程序,自動設置了類似的啓動選項。

5.2 jdb

啓用了 jdwp 之後, 可以使用各種客戶端來進行調試/遠程調試。

比如 jdb 調試本地 JVM:

jdb -attach 'debug'
jdb -attach 8888

當 jdb 初始化並連接到 Test 之後, 就可以進行 Java 代碼級(Java-level)的調試。

5.3 idea 中使用遠程調試

下面介紹 Idea 中怎樣使用遠程調試。

和常規的 Debug 配置類似, 進入編輯:

添加 Remote(不是 Tomcat 下面的那個 Remote Server):

然後配置端口號, 比如:8888。

然後應用。

點擊debug的那個按鈕即可啓動遠程調試,連上之後就和調試本地程序一樣了。當然,記得加斷點或者條件斷點。

注意: 遠程調試時, 需要保證服務端JVM中運行的代碼和本地完全一致,否則可能會有莫名其妙的問題。

細心的同學可能已經發現, Idea 給出了遠程 JVM 的啓動參數, 建議使用 agentlib 的方式::

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888

如果知道這個技巧, 就不用了特殊記憶了。需要的時候找一找即可。

6. JMX 與相關工具

在 Java SE 5 之前,雖然 JVM 提供了一些底層的API,比如 JVMPI 和 JVMTI ,但這些API是面向 C 語言的,需要通過 JNI 等方式才能調用,要監控JVM 和系統資源非常不方便。

Java SE 5 版本中引入了 JMX 技術(Java Management Extensions, Java 管理擴展),用來暴露一些相關信息,甚至支持遠程動態設置t:none;-webkit-font-smoothing:antialiased;box-sizing:border-box;margin:0px 0px 1.1em;outline:0px;">如果你是框架開發者,或者連接池的開發者,還可以註冊MBean到JVM,隨着其他 JMX 的 Bean 一起暴露出去,比如某些監控信息,此處不講解,可以上網搜索。

客戶端使用 JMX 大約支持兩種方式:

  • 從 JVM 運行時獲取 GC 行爲數據, 最簡單的辦法是使用標準 JMX API 接口. JMX 是獲取 JVM 內部運行時狀態信息 的標準API. 可以編寫程序代碼, 通過 JMX API 來訪問本程序所在的 JVM,也可以通過JMX客戶端執行(遠程)訪問。

6.1 程序中獲取 JMX 信息

相關的 MXBean 類位於 rt.jar 文件的 java.lang.management 包中,JDK 中默認就提供了。

代碼獲取 JVM 相關的 MXBean 信息:

// import java.lang.management.\*
// 1\. 操作系統信息
OperatingSystemMXBean operatingSystemMXBean = ManagementFactory.getOperatingSystemMXBean();
// 2\. 運行時
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
// 3.1 JVM內存信息
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
// 3.2 JVM內存池-列表
List<MemoryPoolMXBean> memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans();
// 3.3 內存管理器-列表
List<MemoryManagerMXBean> memoryManagerMXBeans = ManagementFactory.getMemoryManagerMXBeans();
// 4\. class加載統計信息
ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean();
// 5\. 編譯統計信息
CompilationMXBean compilationMXBean = ManagementFactory.getCompilationMXBean();
// 6\. 線程
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 7.GC
List<GarbageCollectorMXBean> garbageCollectorMXBeans 
    = ManagementFactory.getGarbageCollectorMXBeans();

取得這些MXBean之後,就可以獲取對應的Java運行時信息,可以定時上報給某個系統,那麼一個簡單的監控就創建了。

當然,這麼簡單的事情,肯定有現成的輪子啦。比如 Spring Boot Actuator, 以及後面介紹的Micrometer等。 各種監控服務提供的 Agent-lib 中也會採集對應的數據。

6.2 JMX 遠程連接

最常見的 JMX客戶端是 JConsole 和 JVisualVM (可以安裝各種插件,十分強大)。兩個工具都是標準 JDK 的一部分,而且很容易使用。 如果使用的是 JDK7u40 及更高版本,還可以使用另一個工具: Java Mission Control(jmc,大致翻譯爲 Java 控制中心)。

監控本地的 JVM 並不需要額外配置,如果是遠程監控,還可以在服務端部署 jstatd 工具暴露部分信息給 JMX 客戶端。

所有 JMX客戶端都是獨立的程序,可以連接到目標 JVM 上。目標 JVM 可以在本機, 也可能是遠端JVM。

想要支持 JMX 客戶端連接服務端 JVM 實例,則 Java 啓動腳本中需要加上相關的配置參數,示例如下:

-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=10990
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false

如果服務器具有多張網卡(多個IP),由於安全限制,必須明確指定 hostname, 一般是IP。

-Djava.rmi.server.hostname=47.57.227.67

這樣啓動之後,JMX 客戶端(如 JVisualVM)就可以通過 <IP:端口> 連接。(參考 JVisualVM 的示例)。

如這裏對應的就類似於: 47.57.227.67:10990

如果想要遠程查看 VisualGC,則服務端需要開啓 jstatd 來支持, jvisualvm 先連 jstatd 遠程主機,接着在遠程主機上點右鍵添加 jmx 連接。

關於 JVisualVM 的使用,請參考前面的小節。

7. GC 日誌解讀與分析

GC 基礎,GC 性能調優,以及OutOfMemoryError,可參考鐵錨的 CSDN 專欄: GC 性能優化

因爲 GC 日誌模塊內置於JVM中, 所以日誌中包含了對GC活動最全面的描述。 這就是事實上的標準, 可作爲GC性能評估和優化的最真實數據來源。

GC 日誌一般輸出到文件之中, 是純 text 格式的, 當然也可以打印到控制檯。有多個可以控制 GC 日誌的 JVM 參數。例如,可以打印每次GC的持續時間, 以及程序暫停時間(-XX:+PrintGCApplicationStoppedTime), 還有GC清理了多少引用類型(-XX:+PrintReferenceGC)。

通過日誌內容也可以得到 GC 相關的信息。

要打印 GC 日誌, 需要在啓動腳本中指定類似的啓動參數:

-verbose:gc
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:\<filename\>

各個垃圾收集器的日誌可能有一些差異,但只要瞭解大致的套路之後,即使有差異也不會很大。

7.1 示例1

指定 GC 日誌相關的啓動選項,輸出的 GC 日誌就類似於下面這種格式(爲了顯示方便,已手工折行):

2019-07-14T14:45:37.987+0800: 151.126: 
  [GC (Allocation Failure) 151.126: [DefNew: 629119K-\>69888K(629120K), 0.0584157 secs]
    1619346K->1273247K(2027264K), 0.0585007 secs] 
  [Times: user=0.06 sys=0.00, real=0.06 secs]

2019-07-14T14:45:59.690+0800: 172.829: 
  [GC (Allocation Failure) 172.829: [DefNew: 629120K-\>629120K(629120K), 0.0000372 secs]
    172.829: [Tenured: 1203359K->755802K(1398144K), 0.1855567 secs]
    1832479K->755802K(2027264K),
    [Metaspace: 6741K-\>6741K(1056768K)], 0.1856954 secs]
  [Times: user=0.18 sys=0.00, real=0.18 secs]

上面的 GC 日誌暴露了 JVM 中的一些信息。事實上,這個日誌片段中發生了 2 次垃圾收集事件(Garbage Collection events)。其中一次清理的是年輕代(Young generation), 而第二次處理的是整個堆內存。

下面我們來看,如何解讀第一次 GC 事件,發生在年輕代中的小型GC(Minor GC):

  1. 2019-07-14T14:45:37.987+0800 – GC 事件(GC event)開始的時間點.
  2. 151.126 – GC 時間的開始時間,相對於JVM的啓動時間,單位是秒(Measured in seconds).
  3. GC – 用來區分(distinguish)是 Minor GC 還是 Full GC 的標誌(Flag). 這裏的 GC 表明本次發生的是 Minor GC.
  4. Allocation Failure – 引起垃圾回收的原因. 本次 GC 是因爲年輕代中沒有任何合適的區域能夠存放需要分配的數據結構而觸發的.
  5. DefNew – 使用的垃圾收集器的名字. DefNew 這個名字代表的是: 單線程(single-threaded), 採用標記複製(mark-copy)算法的, 使整個JVM暫停運行(stop-the-world)的年輕代(Young generation) 垃圾收集器(garbage collector).
  6. 629119K->69888K – 在本次垃圾收集之前和之後的年輕代內存使用情況(Usage).
  7. (629120K) – 年輕代的總的大小(Total size).
  8. 1619346K->1273247K – 在本次垃圾收集之前和之後整個堆內存的使用情況(Total used heap).
  9. (2027264K) – 總的可用的堆內存(Total available heap).
  10. 0.0585007 secs – GC事件的持續時間(Duration),單位是秒.
  11. [Times: user=0.06 sys=0.00, real=0.06 secs] – GC事件的持續時間,通過多種分類來進行衡量:
  • user – 此次垃圾回收, 各個垃圾收集線程消耗的總CPU時間(Total CPU time).
  • sys – 操作系統調用(OS call) 以及等待系統事件的時間(waiting for system event)
  • real – 應用程序暫停的時間(Clock time). 由於串行垃圾收集器(Serial Garbage Collector)使用單個線程, 所以 real time 等於 user + sys 的和.

通過上面的分析, 可以計算出在垃圾收集期間,JVM 中的內存使用情況。在垃圾收集之前, 堆內存總的使用了 1.54G (1,619,346K)。其中, 年輕代使用了 614M(629,119k)。也就可以推算出老年代使用的內存爲: 967M(990,227K)。

-> 右邊的數據中蘊含了更重要的信息, 年輕代的內存使用量,在垃圾回收後下降了 546M(559,231k), 但總的堆內存使用(total heap usage)只減少了 337M(346,099k). 通過這一點可以算出, 有 208M(213,132K) 的年輕代對象被提升到老年代(Old)中。

不知道各位同學有沒有注意到一些套路。 GC日誌中展示的內存數值,一般都是使用量和總大小,猜一猜就知道各個部分的含義了。

第二次 GC 的日誌,各位同學可以自己分析一下。

7.2 示例2

再來看一個 ParallelGC 輸出的日誌示例:

2019-07-14T14:46:28.829+0800: 200.659: 
  [Full GC (Ergonomics) [PSYoungGen: 64000K-\>63999K(74240K)] 
    [ParOldGen: 169318K-\>169318K(169472K)] 233318K->233317K(243712K), 
    [Metaspace: 20427K-\>20427K(1067008K)], 
  0.1538778 secs] 
  [Times: user=0.42 sys=0.00, real=0.16 secs]

分析以上日誌內容, 可以得知:

  • 這部分日誌截取自JVM啓動後200秒左右。
  • 日誌片段中顯示, 發生了 Full GC。
  • 這次暫停事件的總持續時間是 160毫秒
  • 在GC完成之後, 幾乎所有的老年代空間依然被佔用(169318K->169318K(169472K))。

通過日誌信息可以確定, 該應用的 GC 情況非常糟糕。而GC的結果是, 老年代空間仍然被佔滿.

從此示例可以看出, GC日誌對監控 GC 行爲和 JVM 是否處於健康狀態非常有效。

一般情況下, 查看 GC 日誌就可以快速確定以下症狀:

  • GC 開銷太大。如果 GC 暫停的總時間很長, 就會損害系統的吞吐量。不同的系統允許不同比例的 GC 開銷, 但一般認爲, 正常範圍在 10% 以內。
  • 極個別的 GC 事件暫停時間過長。當某次 GC 暫停時間太長, 就會影響系統的延遲指標. 如果延遲指標規定交易必須在 1,000 ms內完成, 那就不能容忍任何超過 1000毫秒的 GC 暫停。
  • 老年代的使用量超過限制。如果老年代空間在 Full GC 之後仍然接近全滿, 那麼 GC 就成爲了性能瓶頸, 可能是內存太小, 也可能是存在內存泄漏。這種症狀會讓 GC 的開銷暴增。

7.3 GCViewer 工具

可以看到,GC日誌中的信息非常詳細。但除了這些簡單的小程序, 生產系統一般都會生成大量的GC日誌, 純靠人工是很難閱讀和進行解析的。

我們可以自己編寫解析器, 來將龐大的 GC 日誌解析爲直觀易讀的圖形信息。 但很多時候自己寫程序也不是個好辦法,因爲各種 GC 算法的複雜性, 導致日誌信息格式互相之間不太兼容。那麼神器來了: GCViewer

GCViewer 是一款開源的 GC 日誌分析工具。GitHub項目主頁對各項指標進行了完整的描述. 下面我們介紹最常用的一些指標。

gcviewer 本身是一個jar文件,使用的命令大致如下:

java -jar gcviewer\_1.3.4.jar gc.log

大致會看到類似下面的圖形界面:

上圖中, Chart 區域是對GC事件的圖形化展示。包括各個內存池的大小和 GC 事件。上圖中, 只有兩個可視化指標: 藍色線條表示堆內存的使用情況, 黑色的 Bar 則表示每次 GC 暫停時間的長短。

從圖中可以看到, 內存使用量增長很快。一分鐘左右就達到了堆內存的最大值. 堆內存幾乎全部被消耗, 不能順利分配新對象, 並引發頻繁的 Full GC 事件. 這說明程序可能存在內存泄露, 或者啓動時指定的內存空間不足。

從圖中還可以看到 GC暫停的頻率和持續時間。30秒之後, GC幾乎不間斷地運行,最長的暫停時間超過1.4秒

在右邊有三個選項卡。“Summary(摘要)” 中比較有用的是 “Throughput”(吞吐量百分比) 和 “Number of GC pauses”(GC暫停的次數), 以及“Number of full GC pauses”(Full GC 暫停的次數). 吞吐量顯示了有效工作的時間比例, 剩下的部分就是GC的消耗。

以上示例中的吞吐量爲 6.28%。這意味着有 93.72% 的CPU時間用在了GC上面. 很明顯系統所面臨的情況很糟糕 —— 寶貴的CPU時間沒有用於執行實際工作, 而是在試圖清理垃圾。

Pause” 展示了GC暫停的總時間,平均值,最小值和最大值, 並且將 total 與minor/major 暫停分開統計。如果要優化程序的延遲指標, 這些統計可以很快判斷出暫停時間是否過長。另外, 我們可以得出明確的信息: 累計暫停時間爲 634.59 秒, GC暫停的總次數爲 3,938 次, 這在11分鐘/660秒的總運行時間裏那不是一般的高。

更詳細的GC暫停彙總信息, 請查看主界面中的 “Event details” 標籤:

從“Event details” 標籤中, 可以看到日誌中所有重要的GC事件彙總: 普通GC停頓Full GC 停頓次數, 以及併發執行數, 非 stop-the-world 事件等。此示例中, 可以看到一個明顯的地方, Full GC 暫停嚴重影響了吞吐量和延遲, 依據是: 3,928 次 Full GC, 暫停了可以看到, GCViewer 能用圖形界面快速展現異常的GC行爲。一般來說, 圖像化信息能迅速揭示以下症狀:


  * 低吞吐量。當應用的吞吐量下降到不能容忍的地步時, 有用工作的總時間就大量減少. 具體有多大的 “容忍度”(tolerable) 取決於具體場景。按照經驗, 低於 90% 的有效時間就值得警惕了, 可能需要好好優化下GC。
  * 單次GC的暫停時間過長。只要有一次GC停頓時間過長,就會影響程序的延遲指標. 例如, 延遲需求規定必須在 1000 ms以內完成交易, 那就不能容忍任何一次GC暫停超過1000毫秒。
  * 堆內存使用率過高。如果老年代空間在 Full GC 之後仍然接近全滿, 程序性能就會大幅降低, 可能是資源不足或者內存泄漏。這種症狀會對吞吐量產生嚴重影響。

  業界良心 —— 圖形化展示的GC日誌信息絕對是我們重磅推薦的。不用去閱讀冗長而又複雜的GC日誌,通過容易理解的圖形, 也可以得到同樣的信息。

### 8\. 內存 dump 和內存分析工具介紹

  內存 Dump 分爲2種方式: 主動 Dump 和被動 Dump。

  主動 Dump 的工具包括: `jcmd`, `JVisualVM`等等。具體使用請參考相關工具部分。

  被動 Dump 主要是: hprof, 以及 `-XX:+HeapDumpOnOutOfMemoryError` 等參數。

  常用的分析工具有:

  * jhat jhat用來支持分析dump文件, 是一個HTTP/HTML服務器,能將dump文件生成在線的HTML文件,通過瀏覽器查看,一般很少使用。
  * MAT 分析JVM的 Dump文件, 最好用的工具是 `mat`, 全稱是 `Eclipse Memory Analyzer` Tools。 優勢在於, 可以從 GC root 進行對象引用分析, 計算各個 root 所引用的對象有多少, 比較容易定位內存泄露。 MAT是一款獨立的產品, 100MB不到, 可以從官方下載。 下載地址: <https://www.eclipse.org/mat/>

### MAT 使用示例

  現象描述: 系統進行慢SQL優化調整之後上線。 在測試環境沒有發現什麼問題。但運行一段時間之後發現CPU跑滿, 查看GC日誌

  查看本機的Java進程:

jps -v


  假設jps查看到的pid爲3826。

  Dump內存:

jmap -dump:format=b,file=3826.hprof 3826


  導出完成後, dump文件大約是3個G。所以需要修改MAT的配置參數,太小了不行,但也不一定要設置得非常大。

  在MAT安裝目錄下。

  > MemoryAnalyzer.ini

  默認的內存配置是 1024MB, 分析3GB的dump文件可能會報錯。

-vmargs
-Xmx1024m


  根據Dump文件的大小, 適當增加最大堆內存設置, 要求是4MB的倍數, 例如:

-vmargs
-Xmx4g


  雙擊打開 MemoryAnalyzer.exe 

  選擇菜單 File --\> Open File... 選擇對應的 dump 文件。

  選擇 Leak Suspects Report 並確定, 分析內存泄露方面的報告。

  然後等待, 分析完成後, 彙總信息如下:

  佔用內存最大的問題根源1:

  佔用內存最大的問題根源2:

  佔用內存最大的問題根源3:

  可以看到, 總的內存佔用才2GB左右。問題根源1和根源2, 每個佔用 800MB, 問題很可能就在他們身上。 當然, 根源3也有一定的參考價值, 表明這時候有很多JDBC操作。

  查看問題根源1。 其說明信息如下:

The thread org.apache.tomcat.util.threads.TaskThread
@ 0x6c4276718 http-nio-8086-exec-8
keeps local variables with total size 826,745,896 (37.61%) bytes.

The memory of
“org.apache.tomcat.util.threads.TaskThread”
loaded by this Thread is available. See stacktrace.

Keywords
java.net.URLClassLoader @ 0x6c0015a40
org.apache.tomcat.util.threads.TaskThread


org.apache.tomcat.util.threads.TaskThread, 持有了 大約 826MB 的對象, 佔比爲


所有運行中的線程(棧)都是GC-Root。

See stacktrace.

鏈接, 查看導出時的線程調用棧。

節選如下:

Thread Stack

http-nio-8086-exec-8
  ...
  at org.mybatis.spring.SqlSessionTemplate.selectOne
  at com.sun.proxy.$Proxy195.countVOBy(Lcom/\*\*\**/domain/vo/home/residents/ResidentsInfomationVO;)I (Unknown Source)
  at com.\*\*\**.bi.home.service.residents.impl.ResidentsInfomationServiceImpl.countVOBy(....)Ljava/lang/Integer; (ResidentsInfomationServiceImpl.java:164)
  at com.\*\*\**.bi.home.service.residents.impl.ResidentsInfomationServiceImpl.selectAllVOByPage(....)Ljava/util/Map; (ResidentsInfomationServiceImpl.java:267)
  at com.\*\*\*\*.web.controller.personFocusGroups.DocPersonFocusGroupsController.loadPersonFocusGroups(....)Lcom/\*\***/domain/vo/JSONMessage; (DocPersonFocusGroupsController.java:183)
  at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run()V (TaskThread.java:61)
  at java.lang.Thread.run()V (Thread.java:745)

其中比較關鍵的信息, 就是找到我們自己的 package, 如: com.****.....ResidentsInfomationServiceImpl.selectAllVOByPage

並且其中給出了Java源文件所對應的行號。

分析問題根源2, 結果和根源1基本上是一樣的。

當然, 還可以分析這個根源下持有的各個類的對象數量。

點擊根源1說明信息下面的 Details » 鏈接, 進入詳情頁面。

查看其中的 “Accumulated Objects in Dominator Tree”

可以看到佔用內存最多的是2個 ArrayList 對象。

鼠標左鍵點擊第一個 ArrayList 對象, 在彈出的菜單中選擇 “Show objects by class” --> “by outgoing references”。

打開 class_references 標籤頁。

展開後發現 PO 類對象有 113 萬個。加載的確實有點多。直接佔用170MB內存(每個對象約150字節。)

事實上,這是將批處理任務,放到實時的請求中進行計算,導致的問題。

MAT還提供了其他信息, 都可以點開看看, 以增加了解。 碰到不懂的就上網搜索。

9. 面臨複雜問題時可選的高級工具

OOM Killer

內存溢出(Out of Memory,OOM), 是指計算機的所有可用內存(包括交換空間, swap space), 都被使用滿了。

這種情況下, 默認配置會導致系統報警, 並停止正常運行. 當然, 將 /proc/sys/vm/paniconoom 參數設置爲 0, 則告訴系統內核, 如果系統發生內存溢出, 就可以調用 oom_killer(OOM終結者)功能, 來殺掉最胖的那頭進程(rogue processes, 流氓進程), 這樣系統又可以繼續工作了。

假如物理內存不足, Linux 會找出一頭比較壯的進程來殺掉。查看OOM終結者日誌:

Linux系統的OOM終結者, Out of memory killer。

sudo cat /var/log/messages | grep killer -A 2 -B 2

BTrace

BTrace是基於Java語言的一個安全的、可提供動態追蹤服務的工具。

BTrace基於ASM、Java Attach Api、Instruments開發,爲用戶提供了很多註解。依靠這些註解,我們可以編寫BTrace腳本(簡單的Java代碼)達到我們想要的效果(只讀監控),而不必深陷於ASM對字節碼的操作中不可自拔。

細心的同學可能已經發現,在介紹 JVisualVM 的插件時, 截圖中有相關的插件 “BTrace Workbench”。 安裝插件之後,在對應的JVM實例上點右鍵,很容易就進入操作界面了。

BTrace 提供了很多示例,簡單的監控照着改一改就行。

可參考: Java動態追蹤技術探究

Arthas(阿爾薩斯)是Alibaba開源的Java診斷工具,深受開發者喜愛。

當你遇到以下類似問題而束手無策時,Arthas可以幫助你解決:

  1. 遇到問題無法在線上 debug,難道只能通過加日誌再重新發布嗎?
  2. 線上遇到某個用戶的數據處理有問題,但線上同樣無法 debug,線下無法重現!
  3. 是否有一個全局視角來查看系統的運行狀況?
  4. 有什麼辦法可以監控到JVM的實時運行狀態?

Arthas支持JDK 6+,支持Linux/Mac/Winodws,採用命令行交互模式,同時提供豐富的 Tab 自動補全功能,進一步方便進行問題的定位和診斷。

Arthas官方網站提供了在線的命令行模擬器,跟着執行一遍很容易瞭解相關的功能。

10. 應對容器時代面臨的挑戰

在如今的時代,容器的使用越來越普及,成爲很多大規模集羣的基石。 在容器環境下,要直接進行調試並不容易【當然可能性還是有的】。我們更多的是進行應用性能指標的採集和監控,並構建預警機制。而這需要架構師、開發、測試、運維人員的協作。 但監控領域的工具, 又多又雜, 而且在持續發展和迭代中。

最早期的監控, 只在系統發佈時檢查服務器相關的參數,並將這些參數用作系統運行狀況的指標。而監控服務器的健康狀況,與用戶體驗之間緊密相關,悲劇在於那些年代發生的問題比實際檢測到的要多很多。

隨着時間推移,日誌管理、預警、遙測以及系統報告領域持續發力。其中有很多有效的措施, 諸如安全事件, 有效警報, 記錄資源使用量等等, 但前提是我們需要有一個清晰的策略,進行用戶訪問鏈路跟蹤. 比如 Zabbix, Nagios, 以及 Prometheus 等工具在生產環境中被廣泛使用。

性能問題的關鍵是人, 也就是我們的用戶。但已有的這些工具並沒有實現真正的用戶體驗監控。僅僅使用這些軟件也不能緩解性能問題, 我們還需要採取各種措施, 在勇敢和專注下不懈地努力。

Web系統的問題診斷和性能調優,是一件意義重大的事情。需要嚴格把控,也需要付出很多精力。當然, 成功實施這些工作對企業的回報也是巨大的!

Spring是Java領域事實上的標準,SpringBoot提供了一款應用指標收集器: Micrometer,官方文檔連接: https://micrometer.io/docs

  • 支持直接將數據上報給 Elasticsearch, Datadog, InfluxData等各種流行的監控系統。
  • 自動採集最大延遲,平均延遲,95%線,吞吐量,內存使用量等指標。

此外,在小規模集羣中,我們還可以使用Pinpoint 、Skywalking等開源APM工具。

相關鏈接

  1. 官方troubleshoot指南: https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/toc.html
  2. JDK輔助工具參考文檔 : https://docs.oracle.com/javase/8/docs/technotes/tools/unix/index.html
  3. HotSpot VM選項: https://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html
  4. JMX 配置指南: https://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html
  5. GC性能優化系列: https://renfufei.blog.csdn.net/column/info/14851/
  6. GC調優指南: https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/
  7. 延遲(Latency): https://bravenewgeek.com/everything-you-know-about-latency-is-wrong/
  8. CAPACITY TUNING: https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/performance_tuning_guide/s-memory-captun
  9. JVMInternals : http://blog.jamesdbloom.com/JVMInternals.html
  10. JDWP 協議及實現: https://www.ibm.com/developerworks/cn/java/j-lo-jpda3/

歡迎關注我的公衆號,回覆關鍵字“大禮包” ,將會有大禮相送!!! 祝各位面試成功!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章