如何排查java應(yīng)用中CPU使用率高或內(nèi)存占用高的問題?這類問題的排查步驟基本通用的。現(xiàn)在通過一個(gè)具體的例子來說明。
問題描述
最近有個(gè)線上項(xiàng)目每天0點(diǎn)過后CPU使用率會(huì)上升至200%到300%。
排查過程
本節(jié)內(nèi)容是對(duì)排查過程的復(fù)盤,過程記錄會(huì)比較詳細(xì)。如果想知道具體的命令操作,可以直接看總結(jié)部分內(nèi)容。
1)當(dāng)CPU再次暴漲的時(shí)候,首先我們可以通過top -c
查看CPU使用率高的進(jìn)程的PID。
2)然后使用top -p PID -H
查看CPU使用率高的線程信息。如果CPU使用率高的線程是比較固定的,那么我們記下對(duì)應(yīng)線程的PID。
執(zhí)行top -p 14639 -H
得出下圖結(jié)果:
記下4個(gè)線程的PID: 14643、14644、14641、14642
3)接下來通過jstack PID > xxx.log
輸出java應(yīng)用當(dāng)前堆棧信息到文件。
4)第2步中,我們記下了CPU使用率高的線程PID,現(xiàn)在將4個(gè)線程的PID轉(zhuǎn)成16進(jìn)制: 3933、3934、3931、3932。接著在jstack輸出的堆棧文件里,搜索nid等于3933、3934、3931、3932的線程信息。如下圖:
從圖中可以看出,對(duì)應(yīng)的是GC線程。GC消耗大,那就有可能是由于內(nèi)存不足,頻繁執(zhí)行Full GC導(dǎo)致的。
再使用
jstat -gc PID
查看jvm的GC情況,連續(xù)執(zhí)行4次jstat -gc 14639
命令,發(fā)現(xiàn)FGC的數(shù)值變化比較快。這就說明Full GC確實(shí)執(zhí)行很頻繁。如下圖:5)從第1步的截圖中,可以看到CPU高的時(shí)候整個(gè)項(xiàng)目的內(nèi)存占用1.3G左右。既然是內(nèi)存問題,那么就需要使用jmap -histo:live PID > xxx.log
分析下jvm內(nèi)存存活對(duì)象的統(tǒng)計(jì)情況。如下圖:
從圖中可以看出,byte對(duì)象([B)內(nèi)存占用特別高,而且出現(xiàn)了一個(gè)具體的類:ByteArrayRow。這是一個(gè)jdbc做查詢時(shí)候封裝數(shù)據(jù)用的一個(gè)類,這個(gè)類里包含有byte數(shù)組。通過這個(gè)統(tǒng)計(jì)結(jié)果初步懷疑是做數(shù)據(jù)庫查詢時(shí)候,查詢了太多內(nèi)容到了內(nèi)存,導(dǎo)致了內(nèi)存不足。由于統(tǒng)計(jì)中沒有出現(xiàn)具體的業(yè)務(wù)類,所以就以為只是請(qǐng)求量比較大,導(dǎo)致的內(nèi)存消耗過大。當(dāng)時(shí)暫時(shí)將jvm的堆內(nèi)存增大到2G。
6)應(yīng)用jvm堆內(nèi)存調(diào)大之后,到了0點(diǎn)還是出現(xiàn)了CPU高漲的問題。
內(nèi)存占用了2G多,按照目前項(xiàng)目的請(qǐng)求量來說,2G內(nèi)存不可能被占滿了,所以說明并不是請(qǐng)求量大導(dǎo)致的結(jié)果,而是由于某塊代碼查詢數(shù)據(jù)量過大導(dǎo)致的問題。
7)再次運(yùn)行jmap -histo:live PID > xxx.log
將內(nèi)存對(duì)象統(tǒng)計(jì)情況輸出到文件。結(jié)果如下圖:
這次的輸出結(jié)果出現(xiàn)了業(yè)務(wù)類MiniProgram_User_Info,那就可以針對(duì)這個(gè)業(yè)務(wù)類去排查異常代碼的位置了。不過,除非比較清楚這個(gè)類具體使用的地方,否則即使出現(xiàn)了具體的類名還是比較難定位異常代碼的位置。
這時(shí)候,我們可以使用
jmap -dump:live,format=b,file=xxx.hprof PID
命令來輸出內(nèi)存對(duì)象的明細(xì),來定位具體方法位置。這個(gè)命令是將內(nèi)存里的所有信息都輸出出來,輸出的文件大小和內(nèi)存大小基本一致。而且這個(gè)命令會(huì)導(dǎo)致應(yīng)用暫時(shí)掛起,所以謹(jǐn)慎使用。
8)這次將內(nèi)存明細(xì)輸出之后,dump文件大小為2G。用jdk自帶的jhat命令可以分析。之前分析其他dump文件用jhat還是比較方便的。不過,分析這次的dump文件,給了10G運(yùn)行內(nèi)存給jhat命令才勉強(qiáng)打開了文件:jhat -J-mx10G -port 7170
。而且內(nèi)存對(duì)象比較多,查找問題不方便。最后找到了一款神器: jprofiler。用jprofier分析dump文件需要的運(yùn)行內(nèi)存比較少,而且問題定位很方便。很快就定位出了內(nèi)存中的大對(duì)象,占用了1G多內(nèi)存的對(duì)象:
大對(duì)象對(duì)應(yīng)的線程堆棧:
如上圖,至此問題已經(jīng)定位完成了。最后排查代碼,最終發(fā)現(xiàn)凌晨時(shí)候,會(huì)將數(shù)據(jù)庫里100多萬條數(shù)據(jù)查詢出來。內(nèi)存不足導(dǎo)致頻繁GC,結(jié)果就是CPU使用率暴漲。
總結(jié)
一、在排查問題的過程中針對(duì)CPU的問題,使用以下命令組合來排查問題
1、查看問題進(jìn)程,得到進(jìn)程PID:
top -c
2、查看進(jìn)程里的線程明細(xì),并手動(dòng)記下CPU異常的線程PID:
top -p PID -H
3、使用jdk提供jstack命令打印出項(xiàng)目堆棧:
jstack pid > xxx.log
線程PID轉(zhuǎn)成16進(jìn)制,與堆棧中的nid對(duì)應(yīng),定位問題代碼位置。
二、針對(duì)內(nèi)存問題,使用以下命令組合來排查問題:
1、查看內(nèi)存中的存活對(duì)象統(tǒng)計(jì),找出業(yè)務(wù)相關(guān)的類名:
jmap -histo:live PID > xxx.log
2、通過簡單的統(tǒng)計(jì)還是沒法定位問題的話,就輸出內(nèi)存明細(xì)來分析。這個(gè)命令會(huì)將內(nèi)存里的所有信息都輸出,輸出的文件大小和內(nèi)存大小基本一致。而且會(huì)導(dǎo)致應(yīng)用暫時(shí)掛起,所以謹(jǐn)慎使用。
jmap -dump:live,format=b,file=xxx.hprof PID
3、 最后對(duì)dump出來的文件進(jìn)行分析。文件大小不是很大的話,使用jdk自帶的jhat命令即可:
jhat -J-mx2G -port 7170
4、dump文件太大的話,可以使用jprofiler工具來分析。jprofiler工具的使用,這里不做詳細(xì)介紹,有興趣可以搜索一下。
三、需要分析GC情況,可以使用以下命令:
jstat -gc PID
這里簡單介紹一下java8里面這個(gè)命令得出的列表各個(gè)列的含義:
S0C:第一個(gè)幸存區(qū)的大小
S1C:第二個(gè)幸存區(qū)的大小
S0U:第一個(gè)幸存區(qū)的使用大小
S1U:第二個(gè)幸存區(qū)的使用大小
EC:伊甸園區(qū)的大小
EU:伊甸園區(qū)的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法區(qū)大小
MU:方法區(qū)使用大小
CCSC:壓縮類空間大小
CCSU:壓縮類空間使用大小
YGC:年輕代垃圾回收次數(shù)
YGCT:年輕代垃圾回收消耗時(shí)間
FGC:老年代垃圾回收次數(shù)
FGCT:老年代垃圾回收消耗時(shí)間
GCT:垃圾回收消耗總時(shí)間
一般會(huì)比較關(guān)注YGC和FGC的次數(shù)。
內(nèi)容補(bǔ)充
1、jstack輸出的堆棧文件可以上傳到下面這個(gè)網(wǎng)站,這個(gè)網(wǎng)站可以對(duì)堆棧內(nèi)容進(jìn)行統(tǒng)計(jì)匯總,方便我們做分析:http://fastthread.io/index.jsp
2、排查過程小節(jié)中的第5步,jmap命令執(zhí)行完后沒有輸出業(yè)務(wù)類,而第7步在卻有。這個(gè)是因?yàn)榈?步操作的時(shí)候只有1G多的內(nèi)存,代碼還沒執(zhí)行到業(yè)務(wù)對(duì)象的封裝,內(nèi)存就不夠了,后續(xù)的代碼無法被執(zhí)行到。第7步操作的時(shí)候內(nèi)存調(diào)整到2G,所以有部分業(yè)務(wù)對(duì)象已經(jīng)被創(chuàng)建了。