故障藐視
接到通知說線上圖表的導(dǎo)出功能異常,導(dǎo)出功能是做成服務(wù)單獨運行的,上面還有定時人任務(wù)在跑,上服務(wù)器看了下運行日志,日志內(nèi)功還在更新定時任務(wù)的運行日志,只是更新的特別慢,這并不是第一次出現(xiàn)這種情況,重啟服務(wù)可以解決,但是同樣的問題不定時的又會重新出現(xiàn),這可忍不了。使用top命令查看服務(wù)器情況,這個服務(wù)所屬的進程占用cpu達到了400%多!

通過PID號找到對應(yīng)的進程名及所在目錄,看看出問題的是哪個服務(wù)進程
最簡單的方法是直接進入pid目錄查看,輸入命令cd /proc/30219,可以很直觀的看到服務(wù)啟動目錄,如下圖

初步結(jié)果判定
上圖可以看到PID為30279的進程占用CPU非常高,這時候我首先就要看看這個進程JVM的gc情況
jstat -gc 30279 5000

下面是對上圖中各列的解釋
S0C:年輕代中第一個survivor(幸存區(qū))的容量 (字節(jié))
S1C:年輕代中第二個survivor(幸存區(qū))的容量 (字節(jié))
S0U:年輕代中第一個survivor(幸存區(qū))目前已使用空間 (字節(jié))
S1U:年輕代中第二個survivor(幸存區(qū))目前已使用空間 (字節(jié))
EC:年輕代中Eden(伊甸園)的容量 (字節(jié))
EU:年輕代中Eden(伊甸園)目前已使用空間 (字節(jié))
OC:Old代的容量 (字節(jié))
OU:Old代目前已使用空間 (字節(jié))
PC:Perm(持久代)的容量 (字節(jié))
PU:Perm(持久代)目前已使用空間 (字節(jié))
YGC:從應(yīng)用程序啟動到采樣時年輕代中g(shù)c次數(shù)
YGCT:從應(yīng)用程序啟動到采樣時年輕代中g(shù)c所用時間(s)
FGC:從應(yīng)用程序啟動到采樣時old代(full gc)gc次數(shù)
FGCT:從應(yīng)用程序啟動到采樣時old代(full gc)gc所用時間(s)
GCT:從應(yīng)用程序啟動到采樣時gc用的總時間(s)
可以看到老年代的空間十分正常,可是持久代(紅框1)的容量已經(jīng)沾滿了,并且不再擴容,想必是已經(jīng)達到了最大容量,紅框2處顯示Full GC的次數(shù)已經(jīng)達到了兩萬多次,并且還在不斷增長,但是并沒有起到什么作用。
到這里問題就確定了,導(dǎo)致這次服務(wù)卡慢的原因是持久代(Perm)溢出導(dǎo)致gc持續(xù)運行,持久代的最大空間83968k換算下來也只有82m,解決辦法是在啟動服務(wù)時增大持久代的空間
-XX:PermSize=250m -XX:MaxPermSize=512m
進一步原因探究
上面已經(jīng)確定了是永久代溢出導(dǎo)致的問題,但是這只是結(jié)果,到現(xiàn)在還不知道造成永久代內(nèi)存溢出的具體原因,如果完全是因為永久代設(shè)置過小,承受不了程序日常運行所需開銷,那增大永久代空間就可以解決問題,可如果是程序代碼本身存在漏洞,那光加大內(nèi)存也是治標不治本。
根據(jù)問題進程id定位問題線程
命令:top -Hp 30279

紅框部分的線程全是問題線程,先記錄下這些線程的PID,挨個將其轉(zhuǎn)換為16進制
命令:printf "%x\n" 30290 30282 30283 30284 30285 30286 30287 30288 30289

打印問題進程堆棧信息
為方便查看,將堆棧信息答應(yīng)到文件,每隔5秒打印一份
命令:jstack 30279 > jstack.dump

分析堆棧信息
隨便打開一個一個jstack.dump文件,在其中查找剛才轉(zhuǎn)化的十六進制線程id,查找內(nèi)容如下

印證了開始的判斷,全是GC線程。
然后向上翻找,查找與項目有關(guān)的代碼,分析五個堆棧文件,找到如下堆棧信息

這段代碼是系統(tǒng)系統(tǒng)的一個報表導(dǎo)出功能,業(yè)務(wù)十分復(fù)雜,關(guān)聯(lián)資源很多,于是到這里判斷是這個導(dǎo)出所需內(nèi)存資源過大,同時系統(tǒng)永久代內(nèi)存又過小,多用戶同時導(dǎo)出報表就可能撐爆持久代。
到這里,本次線上問題排查就算結(jié)束了,并不是程序代碼的問題,是項目永久代空間設(shè)置過小,不能滿足項目所有功能的長時間運行。
額外分享
這里也再記錄一次早前項目卡死問題的堆棧問題信息記錄,如下

定位到的項目代碼如下:

沒看錯,就是代碼在try catch 捕獲異常輸出的時候采用了e.printStackTrance()進行輸出,e.printStackTrance()會將異常堆棧信息存到字符串內(nèi)存空間中,堆棧信息一旦過長,就很可能導(dǎo)致內(nèi)存溢出。一般只在測試階段才使用該方法打印到控制臺,上線后就應(yīng)該使用log對象將錯誤信息輸出到日志文件里去。
在這里分享一篇文章,解釋為什么e.printStackTrance()會導(dǎo)致程序鎖死:https://blog.csdn.net/hy_coming/article/details/88523988
總結(jié)
關(guān)注系統(tǒng)運行時的資源需求,分配合適的資源。
避免在上線的項目中出現(xiàn)e.printStackTrance()這樣的代碼,這可能是剛?cè)腴T學(xué)習(xí)時養(yǎng)成的習(xí)慣,但是不好的習(xí)慣得改!同時在使用其他可能會產(chǎn)生大量字符串的方法時,都得特別注意。