天天看點

基于JDK指令行工具的監控

JVM參數類型大體分為三種:

标準參數,基本每個版本的JVM都有的參數,比較穩定不變

X參數,非标準化的參數,每個JVM版本的都有些不一樣,但是變化較小

XX參數,非标準化的參數,相對不穩定,每個JVM版本的變化都比較大,主要用于JVM調優和Debug

常見的标準參數:

-help

-server

-client

-version

-showversion

-cp

-classpath

常見的X參數:

-Xint : 解釋執行

-Xcomp : 第一次使用就編譯成本地代碼

-Xmixed : 混合模式,JVM自己來決定是否編譯成本地代碼,這是預設的模式

XX參數又分為兩大類,一種是Boolean類型,如下:

格式 :-XX : [ + - ] < name > 表示啟用或禁用name屬性 比如: -XX:+UseConcMarkSweepGC 表示啟用UseConcMarkSweepGC -XX:+UseG1GC 表示啟用UseG1GC

另一種則是key/value類型的,如下:

格式:-XX : < name > = < value > 表示name屬性的值是value -XX:MaxGCPauseMillis=500 表示MaxGCPauseMillis屬性的值是500 -XX:GCTimeRatio=19 表示GCTimeRatio屬性的值是19

要說最常見的JVM參數應該是 -Xmx 與 -Xms 這兩個參數,前者用于指定初始化堆的大小,而後者用于指定堆的最大值。然後就是-Xss參數,它用于指定線程的堆棧大小。可以看到這三個參數都是以-X開頭的,它們是-X參數嗎?實際上不是的,它們是XX參數,是屬于一種縮寫形式:

-Xms 等價于 -XX:InitialHeapSize -Xmx 等價于 -XX:MaxHeapSize -Xss 等價于 -XX:ThreadStackSize

檢視JVM運作時的參數是很重要的,因為隻有知道目前運作的參數值,才知道要如何去調優。我這裡的伺服器跑了一個Tomcat,我們就以這個Tomcat程序來作為一個例子,該程序的pid是1200,如下:

基于JDK指令行工具的監控

常用的檢視JVM運作時參數:

-XX:+PrintFlagsInitial 檢視初始值

-XX:+PrintFlagsFinal 檢視最終值

-XX:+UnlocakExperimentalVMOptions 解鎖實驗參數

-XX:+UnlocakDiagnosticVMOptions 解鎖診斷參數

-XX:+PrintCommandLineFlags 列印指令行參數

我們來看看<code>-XX:+PrintFlagsInitial</code>參數的使用方式,如下:

加上-version是因為讓它最後的時候輸出版本資訊,不然的話就會輸出幫助資訊了。以上這裡隻是截取了部分的内容,實際列印出來的内容是很多的,大約七百多行。可以看到截取的這部分的參數都是bool類型的(還有其他類型的),而且有 = 和 := 兩種符号,= 表示JVM的預設值, := 表示被使用者或JVM修改的值,也就是非預設值。

注:這種直接使用java指令 + 參數的方式,實際檢視的是目前這條java指令的JVM運作時參數值。

我們來介紹一個指令:jps,這個指令與Linux的ps指令類似,也是檢視程序的,但jps是專門檢視Java程序的,使用也很簡單:

功能描述: jps是用于檢視有權通路的hotspot虛拟機的程序. 當未指定hostid時,預設檢視本機jvm程序,否者檢視指定的hostid機器上的jvm程序,此時hostid所指機器必須開啟jstatd服務。 jps可以列出jvm程序lvmid,主類類名,main函數參數, jvm參數,jar名稱等資訊。

以下簡單示範一下jps指令的常見使用方式:

還需了解更多的話,可以檢視官方的文檔,jps指令的官方文檔位址如下:

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jps.html

如果我們需要檢視一個運作時的Java程序的JVM參數,就可以使用jinfo指令。jinfo是jdk自帶的指令,可以用來檢視正在運作的Java應用程式的擴充參數,甚至支援在運作時,修改部分參數。以下簡單示範一下jinfo指令的常見使用方式:

還需了解更多的話,可以檢視官方的文檔,jinfo指令的官方文檔位址如下:

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jinfo.html#BCGEBFDD

Jstat 用于監控基于HotSpot的JVM,對其堆的使用情況進行實時的指令行的統計,使用jstat我們可以對指定的JVM做如下監控:

類的加載及解除安裝情況

檢視垃圾回收時的資訊

檢視新生代、老生代及持久代的容量及使用情況

檢視新生代、老生代及持久代的垃圾收集情況,包括垃圾回收的次數及垃圾回收所占用的時間

檢視新生代中Eden區及Survior區中容量及配置設定情況等

檢視JIT編譯的資訊

官方文檔位址如下:

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html#BEHHGFAE

檢視類的加載及解除安裝情況的相關選項:

Option

Displays

-class

類加載的行為統計

-class 類加載的行為統計,指令示例:

指令說明:

-class 表示檢視類的加載及解除安裝情況

1200 指定程序的id

1000 指定多少毫秒檢視一次

3 指定檢視多少次,也就是輸出多少行資訊

列印的資訊說明:

Loaded 已加載的類的個數

Bytes 已加載的類所占用的空間大小

Unloaded 已解除安裝的類的個數

Bytes 已解除安裝的類所占用的空間大小

Time 執行類裝載和解除安裝操作所花費的時間

檢視垃圾回收資訊的相關選項:

-gc

垃圾回收堆的行為統計

-gcutil

垃圾回收統計概述(百分比)

-gccause

垃圾收集統計概述(同-gcutil)

-gcnew

新生代行為統計

-gcold

老年代和Metaspace區行為統計

-gccapacity

各個垃圾回收代容量(young,old,perm)和他們相應的空間統計

-gcnewcapacity

新生代與其相應的記憶體空間的統計

-gcoldcapacity

年老代行為統計

-gcmetacapacity

Metaspace區大小統計

-gc 垃圾回收堆的行為統計,指令示例:

列印的資訊說明,C即 Capacity 總容量,U即 Used 已使用的容量:

S0C : survivor0區的總容量

S1C : survivor1區的總容量

S0U : survivor0區已使用的容量

S1U : survivor1區已使用的容量

EC : Eden區的總容量

EU : Eden區已使用的容量

OC : Old區的總容量

OU : Old區已使用的容量

MC : 目前Metaspace區的總容量 (KB)

MU : Metaspace區的使用量 (KB)

CCSC : 壓縮類空間總量

CCSU : 壓縮類空間使用量

YGC : 新生代垃圾回收次數

YGCT : 新生代垃圾回收時間

FGC : 老年代垃圾回收次數

FGCT : 老年代垃圾回收時間

GCT : 垃圾回收總消耗時間

注:我這裡使用的是JDK1.8版本的,如果是其他版本的JDK在這一塊列印的資訊會有些不一樣

JVM大緻的記憶體結構圖(JDK1.8版本):

基于JDK指令行工具的監控

-gccapacity 各個垃圾回收代容量(young,old,perm)和他們相應的空間統計。(同-gc,還會輸出Java堆各區域使用到的最大、最小空間),指令示例:

NGCMN : 新生代占用的最小空間

NGCMX : 新生代占用的最大空間

OGCMN : 老年代占用的最小空間

OGCMX : 老年代占用的最大空間

OGC:目前年老代的容量 (KB)

OC:目前年老代的空間 (KB)

MCMN : Metaspace占用的最小空間

MCMX : Metaspace占用的最大空間

-gcutil 垃圾回收統計概述(同-gc,輸出的是已使用空間占總空間的百分比)。指令示例:

-gccause 垃圾收集統計概述(垃圾收集統計概述(同-gcutil),附加最近兩次垃圾回收事件的原因)。指令示例:

LGCC:最近垃圾回收的原因

GCC:目前垃圾回收的原因

-gcnew(統計新生代行為)。指令示例:

TT:Tenuring threshold(提升門檻值)

MTT:最大的tenuring threshold

DSS:survivor區域大小 (KB)

-gcnewcapacity(新生代與其相應的記憶體空間的統計)。指令示例:

NGC:目前年輕代的容量 (KB)

S0CMX:最大的S0空間 (KB)

S0C:目前S0空間 (KB)

ECMX:最大eden空間 (KB)

EC:目前eden空間 (KB)

-gcold(老年代和Metaspace區行為統計)。指令示例:

-gcoldcapacity(老年代與其相應的記憶體空間的統計)。指令示例:

-gcmetacapacity(Metaspace區與其相應記憶體空間的統計)。指令示例:

檢視JIT編譯資訊的相關選項:

-compiler

HotSpt JIT編譯器行為統計

-printcompilation

HotSpot編譯方法統計

-compiler HotSpt JIT編譯器行為統計,指令示例:

Compiled : 編譯數量

Failed : 編譯失敗數量

Invalid : 無效數量

Time : 編譯耗時

FailedType : 失敗類型

FailedMethod : 失敗方法的全限定名

-printcompilation HotSpot編譯方法統計,指令示例:

Compiled:被執行的編譯任務的數量

Size:方法位元組碼的位元組數

Type:編譯類型

Method:編譯方法的類名和方法名。類名使用"/" 代替 "." 作為空間分隔符. 方法名是給出類的方法名. 格式是一緻于HotSpot -XX:+PrintComplation 選項

我們都知道部署線上上的項目,是不能夠直接修改其代碼或随意關閉、重新開機服務的,是以當發生記憶體溢出錯誤時,我們需要通過監控工具去分析錯誤的原因。是以本小節簡單示範一下JVM堆區和非堆區的記憶體溢出,然後我們再通過工具來分析記憶體溢出的原因。首先使用IDEA建立一個SpringBoot工程,工程的目錄結構如下:

基于JDK指令行工具的監控

我這裡隻勾選了web和Lombok以及增加了asm依賴,因為在示範非堆區記憶體溢出時,我們需要通過asm來動态生成class檔案。是以pom.xml檔案裡所配置的依賴如下:

先來示範堆區的記憶體溢出,為了能夠讓記憶體更快的溢出,是以我們需要設定JVM記憶體參數值。如下:

1、

基于JDK指令行工具的監控

2、

基于JDK指令行工具的監控

建立一個實體類,因為對象是存放在堆區的,是以我們需要有一個實體對象來制造記憶體的溢出。代碼如下:

然後建立一個controller類,友善我們通過postman等工具去進行測試。代碼如下:

啟動SpringBoot,通路 <code>localhost:8080/heap</code> 後,控制台輸出的錯誤日志如下:

基于JDK指令行工具的監控

示範完堆區記憶體溢出後,我們再來看看非堆區的記憶體溢出,從之前的JVM記憶體結構圖可以看到,在JDK1.8中,非堆區就是Metaspace區。同樣的為了能夠讓記憶體更快的溢出,是以我們需要設定JVM的Metaspace區參數值如下:

基于JDK指令行工具的監控

Metaspace區可以存儲class,是以我們通過不斷的存儲class來制造Metaspace區的記憶體溢出。使用asm架構我們可以動态的建立class檔案。建立一個 Metaspace 類,代碼如下:

在 MemoryController 類中增加一個成員變量和一個方法,用于制造非堆區的記憶體溢出。代碼如下:

啟動SpringBoot,通路 <code>localhost:8080/nonheap</code> 後,控制台輸出的錯誤日志如下:

基于JDK指令行工具的監控

上一小節中,我們示範了兩種記憶體溢出,堆區記憶體溢出與非堆區記憶體溢出。如果我們線上的項目出現這種記憶體溢出的錯誤該如何解決?我們一般主要通過分析記憶體映像檔案,來檢視是哪些類一直占用着記憶體沒有被釋放。

導出記憶體映像檔案的幾種方式:

第一種:當發生記憶體溢出時JVM自動導出,這種方式需要設定如下兩個JVM參數:

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=./

第二種:使用jmap指令手動導出,我們一般都是使用這種方式,因為等到當發生記憶體溢出時再導出就晚了,我們應該盡量做到預防錯誤的發生

注:-XX:HeapDumpPath=./ 用于指定将記憶體映像檔案導出到哪個路徑

我們先示範第一種導出記憶體映像檔案的方式,同樣的,需要先設定一下JVM的參數,如下:

基于JDK指令行工具的監控

啟動SpringBoot,通路 <code>localhost:8080/heap</code> 後,控制台輸出的錯誤日志如下,可以看到記憶體映像檔案被導出到目前工程的根目錄了:

基于JDK指令行工具的監控

打開工程的根目錄,就可以看到這個記憶體映像檔案:

基于JDK指令行工具的監控

接着我們再來示範一下使用jmap指令來導出記憶體映像檔案,指令如下:

指令選項說明:

-dump 導出記憶體映像檔案

format 指定檔案為二進制格式

file 指定檔案的名稱,預設導出到目前路徑

因為目前的路徑是在桌面,是以就導出到桌面上了:

基于JDK指令行工具的監控

如果需要了解更多關于jmap的用法,可以查閱官方文檔,位址如下:

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jmap.html#CEGCECJB

在上一小節中,我們已經示範了兩種導出記憶體映像檔案的方式。但是這些記憶體映像檔案裡都是些什麼東西呢?我們要如何利用記憶體映像檔案去分析問題所在呢?那這就需要用到另一個工具MAT了。

MAT是Eclipse的一個記憶體分析工具,全稱Memory Analyzer Tools,官網位址如下:

http://www.eclipse.org/mat/

MAT的下載下傳位址如下:

http://www.eclipse.org/mat/downloads.php

下載下傳并解壓之後,點選MemoryAnalyzer.exe即可打開該工具,并不需要打開Eclipse,雖然下載下傳的壓縮包裡包含了Eclipse:

基于JDK指令行工具的監控

正常打開後界面如下:

基于JDK指令行工具的監控

然後我們打開之前示範的發生記憶體溢出時,JVM自動導出的記憶體映像檔案:

基于JDK指令行工具的監控
基于JDK指令行工具的監控
基于JDK指令行工具的監控

記憶體映像檔案打開後,MAT會自動分析出一個餅狀圖,把可能出現問題的三個地方列了出來,并通過餅狀圖分為了三塊。Problem Suspect 1表示最有可能導緻問題出現的原因所在,而且也可以看到,的确是指向了我們示範記憶體溢出的那個 MemoryController 類。上面也描述了,該類的一個執行個體所占用的記憶體達到了55.57%:

基于JDK指令行工具的監控

這樣我們就很輕易的找到了問題的所在,當然線上環境肯定不會這麼簡單。畢竟這是我們故意去制造的記憶體溢出,如果是實際的生産環境會更複雜一些。

是以我們還會進行更多的分析,例如檢視所有類的執行個體對象的數量:

基于JDK指令行工具的監控

或者檢視指定類的執行個體對象數量,可以看到,User這個類的執行個體對象有十萬多個,一個類的執行個體對象存在十萬多個,肯定是有問題的:

基于JDK指令行工具的監控

右鍵點選這個有問題的對象,檢視其強引用:

基于JDK指令行工具的監控

從下圖中,可以看到首先是Tomcat的一個TaskThread引用了MemoryController,而MemoryController裡包含了一個名為userList的集合類型成員變量,該集合中存放了十萬多個User執行個體對象,這下基本上就可以确定是這個MemoryController裡userList的問題了:

基于JDK指令行工具的監控

除此之外還可以檢視對象所占的位元組數,使用方式和檢視對象數量是一樣的:

基于JDK指令行工具的監控

MAT的常用功能就先介紹到這裡,一般我們使用這些常用功能就已經能夠定位問題的所在了,而且這種圖形化的工具也比較好上手,這裡就不過多贅述了。

jstack可以列印JVM内部所有的線程資料,是java虛拟機自帶的一種線程堆棧跟蹤工具。使用jstack列印線程堆棧資訊時,可以将這些資訊重定向到一個檔案裡,這樣就相當于生成了JVM目前時刻的線程快照。線程快照是目前JVM内每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如線程間死鎖、死循環、請求外部資源導緻的長時間等待等。 線程出現停頓的時候通過jstack來檢視各個線程的調用堆棧,就可以知道沒有響應的線程到底在背景做什麼事情,或者等待什麼資源。 如果java程式崩潰生成core檔案,jstack工具可以用來獲得core檔案的java stack和native stack的資訊,進而可以輕松地知道java程式是如何崩潰和在程式何處發生問題。另外,jstack工具還可以附屬到正在運作的java程式中,看到當時運作的java程式的java stack和native stack的資訊, 如果現在運作的java程式呈現hung的狀态,jstack是非常有用的。

jstack官方文檔位址如下:

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstack.html#BABGJDIF

使用jstack列印Java程式裡所有線程堆棧資訊示例:

注:nid是線程的唯一辨別符,是16進制的,通常用于定位某一個線程

這裡隻是截取了前面兩條線程的資訊,可以看到這些線程都有一個java.lang.Thread.State參數,該參數的值就是該線程的狀态。

Java線程狀态:

NEW 未啟動的新線程

RUNNABLE 正在運作的線程

BLOCKED 阻塞狀态,一般都是在等待鎖資源

WAITING 等待狀态

TIMED_WAITING 有時間的等待狀态

TERMINATED 線程已退出

線程狀态轉換示意圖:

基于JDK指令行工具的監控

本小節我們使用一個例子示範死循環與死鎖,然後介紹如何利用jstack分析、定位問題的所在。

在controller包中,建立一個 CpuController 類,用于示範發生死循環與死鎖時CPU占用率飙高的情況。這是一個解析json的代碼,并不需要注意代碼的細節,隻需要知道通路這個接口會導緻死循環即可。代碼如下:

将工程使用maven進行打包,并上傳到伺服器中,打包指令如下:

mvn clean package -Dmaven.test.skip=true

将jar包上傳到伺服器中,然後使用如下指令進行啟動:

接着使用浏覽器開多幾個标簽頁來通路該工程的接口,為了讓CPU負載更快飚上去:

基于JDK指令行工具的監控

在Linux的指令行輸入top指令來檢視CPU負載情況,等那麼一兩分鐘後,會發現CPU的負載就上去了,如下:

基于JDK指令行工具的監控

當我們伺服器的CPU像這樣負載很高的時候,就可以使用jstack指令去定位哪一個線程的CPU占用率最高。通過jstack指令列印線程的堆棧資訊,并重定向到一個檔案中:

接着使用top指令指定檢視某個程序中的線程:

通過以上這個指令,可以看到該程序中占用率最高的那幾個線程,我們把占用率第一的線程的pid給記錄一下:

基于JDK指令行工具的監控

然後通過printf指令,将pid轉換成16進制的nid,實際上這裡的pid就是十進制的nid,如下:

得出nid後,使用vim指令打開loop.txt檔案,通過nid來搜尋該線程的資料:

基于JDK指令行工具的監控

如上,通過分析線程堆棧的資訊,就能定位到是哪個類的哪個方法裡的哪句代碼出了問題,這就是如何利用jstack指令,定位問題代碼。

以上示範完如何定位發生死循環的代碼後,接下來就是示範一下如何使用jstack定位發生死鎖的代碼。首先,在CpuController類中,增加如下代碼:

增加完以上代碼後,重新使用maven指令進行打包。

回到伺服器上,殺掉之前啟動的服務,并把舊的jar包給删除掉:

删除掉舊的jar包後,再重新上傳新打包好的jar包,然後和之前一樣使用如下指令運作該jar包:

成功運作後,同樣的使用浏覽器進行通路,可以看到是能夠正常傳回資料的,這是因為發生死鎖的是子線程,并不會影響主線程:

基于JDK指令行工具的監控

那麼我們要怎麼定位死鎖發生的代碼呢?因為這種情況下的死鎖和死循環不一樣,并不會導緻CPU負載率的飙高。是以我們無法使用之前那種方式去定位問題代碼,但jstack比較好的一點就是,會自動幫我們找出死鎖。和之前一樣,使用如下指令生成一個線程快照檔案:

使用vim打開該檔案後,直接定位到檔案的末尾,就可以看到死鎖的資訊,jstack會自動找出死鎖,并把死鎖資訊放在末尾。我已經使用藍色和紅色框框标出了兩個線程互相等待的鎖:

基于JDK指令行工具的監控

jdk8工具集

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/index.html

Troubleshooting

https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/

jps

jinfo

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jinfo.html

jstat

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html

jmap:

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jmap.html

mat:

jstack:

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstack.html

java線程的狀态

https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr034.html

java線程狀态轉化:

https://mp.weixin.qq.com/s/GsxeFM7QWuR--Kbpb7At2w

死循環導緻CPU負載高

https://blog.csdn.net/goldenfish1919/article/details/8755378

正規表達式導緻死循環:

https://blog.csdn.net/goldenfish1919/article/details/49123787