技術(shù)干貨實戰(zhàn)(5)- JVM 性能監(jiān)控工具之jstack、jstat等命令
作者:
修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接和本聲明。
日常應(yīng)用巡檢期間,突然發(fā)現(xiàn)線上應(yīng)用服務(wù)器CPU飆高、內(nèi)存占用過高等情況時,傳統(tǒng)的做法是不管三七二十一,直接重啟線上應(yīng)用,一了百了,這種方式雖然有時候可以解決一時的問題,但是卻會讓工程師錯失排錯、揪出問題根源的機會;本文將介紹出現(xiàn)上述等情況時,如何對整個應(yīng)用服務(wù)器的整體性能、Java應(yīng)用服務(wù)進行監(jiān)控、排查以及定位,其中的jvm命令包括:jstack、jstat等等
在現(xiàn)實企業(yè)級Java應(yīng)用開發(fā)、維護中,有時候我們會碰到下面這些問題:
(1)OOM,即OutOfMemoryError,內(nèi)存溢出/內(nèi)存不足;
(2)線程死鎖 或者 鎖爭用(Lock Contention)
(3)Java進程消耗CPU過高,打開云服務(wù)器的性能指標(biāo)監(jiān)控直方圖時會發(fā)現(xiàn)CPU一直居高不下 ......
這些問題在日常開發(fā)、維護中可能被很多人忽視(比如有的人遇到上面的問題只是暴力重啟或者調(diào)大內(nèi)存,而不會深究其問題根源),但說實在的,倘若能夠理解并解決這些問題,那將大大增強Java開發(fā)者的進階技能;
本文將以debug新發(fā)布的新課
“Java核心技術(shù)-典型案例與面試實戰(zhàn)系列二(SpringBoot2.0+企業(yè)真實案例)”中提及的、集群部署下的“權(quán)限管理平臺”項目為案例,部署到Linux ECS Centos7上,并采用JVM相應(yīng)的命令進行監(jiān)控查看,訪問地址為:http://history.huicairj.com/
一、jstack
(1)作用:查看一個java進程內(nèi) 線程占用的堆棧內(nèi)存信息
(2)語法格式:
jstack [option] pid
jstack [option] executable core
jstack [option] [server-id@]remote-hostname-or-ip
參數(shù)額外說明:
-l long listings,會打印出額外的鎖信息,在發(fā)生死鎖時可以用jstack -l pid來觀察鎖持有情況-m mixed mode,不僅會輸出Java堆棧信息,還會輸出C/C++堆棧信息(比如Native方法),如下圖所示為 進程pid 為 13884 對應(yīng)的項目 “權(quán)限管理平臺”內(nèi)各個線程持有鎖的情況,輸入命令后回車,并移動到輸出的信息的最下面即可得到::
目前沒有看到某個線程獨占鎖的情況,即None,也沒有出現(xiàn)某個線程正在等待獲取某個資源Waiting No Object;
一般如果出現(xiàn)死鎖的話,會伴隨著兩個現(xiàn)象:A.某個線程等待獲取某個資源的鎖,即Waiting to lock monitor… B.這個資源卻被另外的線程所持有,即which is held by “Thread-xxxx”,如下圖所示為多年前debug曾經(jīng)的一次日常線上應(yīng)用巡檢時出現(xiàn)的情況:
而很明顯兩個線程在互相爭奪對方的持有的資源,僵持不下,因此也就出現(xiàn)了:死鎖,即dead lock
順提一下,檢測死鎖還可以利用JVM自帶的jconsole工具進行查看(在成功安裝jdk時有個bin目錄,里面的jconsole.exe就是),如下圖所示:
(3)命令實操:言歸正傳,jstack可以定位到線程堆棧,根據(jù)堆棧信息我們可以定位到具體的代碼,所以它在JVM性能調(diào)優(yōu)與監(jiān)控中使用得非常多;
下面以“權(quán)限管理平臺”對應(yīng)的進程:13884為例,查看其線程中最耗費CPU的Java線程并定位堆棧信息,用到的命令有top、printf、jstack、grep等
已經(jīng)確定了進程id為 13884 后,接下來查看該進程內(nèi)部最耗費CPU的線程,命令為:
top -Hp 13884
如下圖所示:
TIME那一列就是各個線程耗費的CPU時間,CPU時間最長的是線程ID為13893的線程,然后執(zhí)行以下命令:
printf "%x" 13893
得到13893的十六進制值為3645,在下面會用到;緊接著輪到jstack上場了,用于輸出進程13893的堆棧信息,然后根據(jù)線程ID的十六進制值grep,如下:
jstack 13884 | grep 3654
回車后出現(xiàn)如下的結(jié)果:
"C2 CompilerThread0" #48 daemon prio=0 os_prio=0 tid=0x00007f45f0b80000 nid=0x188 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
如果是出現(xiàn)這種結(jié)果 且 線上CPU真的飆高的話,那應(yīng)該考慮關(guān)閉下C2編譯器,即只需要在java -jar xx.jar 啟動命令中,加下jvm的參數(shù)即可:-XX:-TieredCompilation –server ,其深層次的含義為:關(guān)閉JIT分層編譯
這玩意比較深奧,感興趣的小伙伴可以網(wǎng)上搜羅一番資料自行學(xué)習(xí)學(xué)習(xí);
另外,有些時候執(zhí)行 jstack 13884 | grep 3654 命令得到的結(jié)果可能是某個 類 在等待執(zhí)行某個方法,那么這個時候確實代碼寫得有問題了,可以根據(jù)返回的結(jié)果提示找到具體的類方法代碼即可!
二、jstat
(1)作用: jstat(JVM統(tǒng)計監(jiān)測工具)可用于查看應(yīng)用所占堆內(nèi)存各個區(qū)的情況以及GC的情況,在實際應(yīng)用期間也是很廣泛的!
(2)語法格式:
jstat [ generalOption | outputOptions vmid [interval[s|ms] [count]] ]
vmid是Java虛擬機的ID,在Linux/Unix系統(tǒng)上一般就是進程ID,interval是采樣時間間隔,count是采樣數(shù)目
在命令實操之前,首先得給大家介紹下JVM的內(nèi)存結(jié)構(gòu),因為待會兒在解釋 執(zhí)行 jstat 命令后打印出來的結(jié)果時需要用到;如下圖所示Java虛擬機給一個應(yīng)用分配的運行時的內(nèi)存結(jié)構(gòu),如下圖所示:
各控制參數(shù)的含義如下所示:
-Xms設(shè)置堆的初始(最?。┛臻g大小。
-Xmx設(shè)置堆的最大空間大小。
-XX:NewSize設(shè)置新生代初始(最?。┛臻g大小。
-XX:MaxNewSize設(shè)置新生代最大空間大小。
-XX:PermSize設(shè)置永久代(方法區(qū))最小空間大小。
-XX:MaxPermSize設(shè)置永久代(方法區(qū))最大空間大小。
-Xss設(shè)置每個線程的堆棧大小。
(注:方法區(qū)在JDK1.7的時候叫做永久代,到JDK1.8之后廢棄了永久代改為元空間(meta space))
而堆內(nèi)存head
space那一塊是我們最為關(guān)心的,因為幾乎所有的對象實例、數(shù)組都存放在java堆里,它也是GC回收重點關(guān)注的地方,其內(nèi)存結(jié)構(gòu)我們單獨抽出來,如下圖所示:
不難看出:
堆內(nèi)存 = 年輕代 + 年老代 + 永久代 (這個有點爭議)
年輕代 = Eden區(qū)(Eden) + 兩個Survivor區(qū)(From和To),即S0 和 S1
默認(rèn)Eden:from :to = 8:1:1
(3)命令實操:如下圖輸出的GC信息中,采樣時間間隔為2s,共采樣60次的結(jié)果:
上圖輸出的信息中各列的含義如下所示(以下的 容量 字段信息
單位為:字節(jié) ):
S0C、S1C、S0U、S1U:Survivor 0/1區(qū)分配的容量(Capacity)和使用量(Used)
EC、EU:Eden區(qū)分配的容量和使用量
OC、OU:年老代容量和使用量
PC、PU:永久代容量和使用量
YGC、YGT:年輕代GC次數(shù)和GC耗時
FGC、FGCT:Full GC次數(shù)和Full GC耗時
GCT:GC總耗時
在上圖中我們可以看到,每隔2s采樣一次,共采樣次數(shù)為60次,在此期間發(fā)生了3次的YGC (因為debug的這臺測試服務(wù)器本身內(nèi)存不高:2g 而已),其發(fā)生的過程在上圖中已經(jīng)都標(biāo)注出來了,諸位可以自行看看,下面再簡單地總結(jié)下GC的相關(guān)概念以及觸發(fā)GC的時機和過程:
(1)基本概念
A. YGC :對新生代堆內(nèi)存進行垃圾回收,即GC,頻率比較高,因為大部分對象的存活壽命較短,在新生代里被回收,性能耗費較小。
B. FGC :全范圍堆內(nèi)存的GC,默認(rèn)堆空間使用到達80%(可調(diào)整)的時候會觸發(fā)FGC,以我們生產(chǎn)環(huán)境為例,一般比較少會觸發(fā)FGC,有時30天或幾個月會有一次;
(2)觸發(fā)GC的時機以及GC過程
A.YGC的時機:Eden空間不足
像上圖所展示的,Eden空間已經(jīng)不足以支撐下次的對象內(nèi)存分配了,因此會觸發(fā)YGC,其GC過程比較簡單:標(biāo)記Eden區(qū)和其中一個S區(qū),如S1仍然存活的對象,然后Copy將其搬遷至另一個S區(qū),如S0 (From 和 To命令的由來),同時釋放Eden和S1內(nèi)存空間,那些存活的對象年齡 +1,YGC次數(shù) +1;
隨著系統(tǒng)的運行,會發(fā)現(xiàn)Eden空間再次不足,此時再次觸發(fā)YGC,其過程也是一個道理,從Eden區(qū)和S0區(qū)標(biāo)記出仍然存活的對象并將其Copy至S1區(qū),釋放空間,對象年齡+1,YGC+1,如此循環(huán)反復(fù),當(dāng)對象年齡達到15時(默認(rèn)是15,可以調(diào)整設(shè)置),即會開始將那些仍然存活的對象搬遷拷貝至年老代Old區(qū)…(以上過程是以標(biāo)記-清除復(fù)制算法為例)
B.FGC的時機:Old空間不足;Perm空間不足;顯示調(diào)用System.gc() ,包括RMI等的定時觸發(fā);YGC時的悲觀策略;dump live的內(nèi)存信息時(jmap –dump:live)。
而對于FGC觸發(fā)的時機則比較多,但常見無非是:Old空間不足 或 Perm空間不足,其GC過程比較復(fù)雜(取決于采用什么GC算法 和 垃圾回收器,經(jīng)典的當(dāng)然是:分代收集算法),在這里debug就不做過多介紹了,感興趣的小伙伴可以網(wǎng)上搜羅一番,參考鏈接:https://www.cnblogs.com/bigbaby/p/12348968.html
總結(jié):
(1)代碼下載:文中涉及到的“權(quán)限管理平臺”項目源碼數(shù)據(jù)庫 可以通過關(guān)注微信公眾號:程序員實戰(zhàn)基地(掃描網(wǎng)站底部的微信公眾號即可),回復(fù)數(shù)字: 101 ,即可下載 !
我是debug,一個相信技術(shù)改變生活、技術(shù)成就夢想 的攻城獅;如果本文對你有幫助,請關(guān)注公眾號,并動動手指收藏、點贊、以及轉(zhuǎn)發(fā)哦?。。?span lang="EN-US">