【JVM性能優化】問題故障排查的解決方案(上)
前提概要
線上故障主要會包括cpu、磁盤、記憶體以及網絡問題,而大多數故障可能會包含不止一個層面的問題,是以進行排查時候盡量四個方面依次排查一遍。同時例如jstack、jmap等工具也是不囿于一個方面的問題的,基本上出問題就是df、free、top三連,然後依次jstack、jmap伺候,具體問題具體分析即可。
CPU的問題
一般來講我們首先會排查cpu方面的問題。cpu異常往往還是比較好定位的。原因包括業務邏輯問題(死循環)、頻繁gc以及上下文切換過多。而最常見的往往是業務邏輯(或者架構邏輯)導緻的,可以使用jstack來分析對應的堆棧情況。
jstack分析cpu問題
- 先用ps指令找到對應程序的pid(如果你有好幾個目标程序,可以先用top看一下哪個占用比較高),來找到cpu使用率比較高的一些線程
top -H -p pid
這裡需要注意的是 -p代表着通過程序号,-H 查詢的是輸出使用率最高線程
- 将占用最高的pid轉換為16進制得到nid
printf ‘%x\n’ pid
- 接着直接在jstack中找到相應的堆棧資訊
jstack ‘0x42’ | grep ‘nid’ -C5 –color
可以看到我們已經找到了nid為0x42的堆棧資訊,接着隻要仔細分析一番即可。
- 排查整個jstack檔案
- 當然更常見的是我們對整個jstack檔案進行分析,通常我們會比較關注WAITING和TIMED_WAITING的部分,BLOCKED就不用說了。
- 使用指令
來對jstack的狀态有一個整體的把握,如果WAITING之類的特别多,那麼多半是有問題啦。cat jstack.log | grep "java.lang.Thread.State" | sort -nr | uniq -c
JVM頻繁gc(FullGC)
使用jstack來分析問題,但有時候我們可以先确定下gc是不是太頻繁,使用
jstat -gc pid 1000
指令來對gc分代變化情況進行觀察,1000表示采樣間隔(ms)。
- S0C/S1C、S0U/S1U、EC/EU、OC/OU、MC/MU分别代表兩個Survivor區、Eden區、老年代、中繼資料區的容量和使用量。
- YGC/YGT、FGC/FGCT、GCT則代表YoungGc、FullGc的耗時和次數以及總耗時。
如果看到gc比較頻繁,再針對gc方面做進一步分析
上下文切換
針對頻繁上下文問題,可以使用vmstat指令來進行檢視。
vmstat 1
代表着1秒列印一次。
- cs(context switch)一列則代表了上下文切換的次數。
如果我們希望對特定的pid進行監控那麼可以使用
pidstat -w pid
指令,cswch和nvcswch表示自願及非自願切換。
磁盤的問題
狀态資訊
- 磁盤問題和cpu一樣是屬于比較基礎的。首先是磁盤空間方面,我們直接使用
來檢視檔案系統狀态df -hl
- 磁盤問題還是性能上的問題。可以通過
來進行分析。iostat -d -k -x
圖檔來源于blog.csdn.net/pengjunlee/…
- 最後一列%util可以看到每塊磁盤寫入的程度,而rrqpm/s以及wrqm/s分别表示讀寫速度,一般就能幫助定位到具體哪塊磁盤出現問題了。
- 另外我們還需要知道是哪個程序在進行讀寫,一般來說開發自己心裡有數,或者用
指令來進行定位檔案讀寫的來源。iotop
不過這邊拿到的是tid,我們要轉換成pid,可以通過readlink來找到pidreadlink -f /proc/*/task/tid/…/…。
圖檔來源于blog.csdn.net/pengjunlee/…
找到pid之後就可以看這個程序具體的讀寫情況cat /proc/pid/io
可以通過lsof指令來确定具體的檔案讀寫情況
lsof -p pid
圖檔來源于blog.csdn.net/pengjunlee/…
記憶體問題
記憶體問題排查起來相對比CPU麻煩一些,場景也比較多。主要包括OOM、GC問題和堆外記憶體。一般來講,我們會先用
free
指令先來檢查一發記憶體的各種情況。
堆内記憶體
記憶體問題大多還都是堆内記憶體問題。表象上主要分為OOM和StackOverflow。
OOM問題
JVM中的記憶體不足,OOM大緻可以分為以下幾種:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
複制代碼
- 沒有足夠的記憶體空間給線程配置設定java棧,基本上還是線程池代碼寫的有問題,比如說忘記shutdown,或者使用無限制的任務隊列,是以說應該首先從代碼層面來尋找問題,使用jstack或者jmap。
- 如果一切都正常,JVM方面可以通過指定Xss來減少單個thread stack的大小。
- 另外也可以在系統層面,可以通過修改/etc/security/limits.conf的,nofile和nproc來增大os對線程的限制
問題
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
複制代碼
原因
這個意思是堆的記憶體占用已經達到-Xmx設定的最大值,應該是最常見的OOM錯誤了。
解決辦法
解決思路仍然是先應該在代碼中找,懷疑存在記憶體洩漏,通過jstack和jmap去定位問題。如果說一切都正常,才需要通過調整Xmx的值來擴大記憶體。
問題
Caused by: java.lang.OutOfMemoryError: Meta space
複制代碼
解決辦法
這個意思是中繼資料區的記憶體占用已經達到
-XX:MaxMetaspaceSize
設定的最大值,排查思路和上面的一緻,參數方面可以通過
-XX:MaxPermSize
來進行調整(這裡就不說1.8以前的永久代了)。
問題
Exception in thread "main" java.lang.StackOverflowError
複制代碼
解決辦法
表示線程棧需要的記憶體大于Xss值,同樣也是先進行排查,參數方面通過Xss來調整,但調整的太大可能又會引起OOM。
- 使JMAP定位代碼記憶體洩漏,上述關于OOM和StackOverflow的代碼排查方面,我們一般使用
來導出dump檔案jmap -dump:format=b,file=filename pid
- 通過MAT(Eclipse Memory Analysis Tools)導入dump檔案進行分析,記憶體洩漏問題一般我們直接選Leak Suspects即可,mat給出了記憶體洩漏的建議。另外也可以選擇Top Consumers來檢視最大對象報告。
- 線程相關的問題可以選擇thread overview進行分析。除此之外就是選擇Histogram類概覽來自己慢慢分析,大家可以搜搜mat的相關教程。
代碼産生記憶體洩漏是比較常見的事,并且比較隐蔽,需要開發者更加關注細節。
- 比如說每次請求都new對象,導緻大量重複建立對象;
- 進行檔案流操作但未正确關閉;手動不當觸發gc,ByteBuffer緩存配置設定不合理等都會造成代碼OOM。
可以在啟動參數中指定
-XX:+HeapDumpOnOutOfMemoryError
來儲存OOM時的dump檔案。
gc問題和線程
- GC問題除了影響cpu也會影響記憶體,排查思路也是一緻的。一般先使用jstat來檢視分代變化情況,比如youngGC或者fullGC次數是不是太多呀;EU、OU等名額增長是不是異常呀等。
- 線程的話太多而且不被及時gc也會引發oom,大部分就是之前說的unable to create new native thread。除了jstack細細分析dump檔案外,我們一般先會看下總體線程,通過
pstree -p pid |wc -l
或者直接通過檢視/proc/pid/task的數量即為線程數量。
堆外記憶體
如果碰到堆外記憶體溢出,那可真是太不幸了。首先堆外記憶體溢出表現就是實體常駐記憶體增長快,報錯的話視使用方式都不确定。
由于使用Netty導緻的,那錯誤日志裡可能會出現OutOfDirectMemoryError錯誤,如果直接是DirectByteBuffer,那會報OutOfMemoryError: Direct buffer memory。
堆外記憶體溢出往往是和NIO的使用相關,一般我們先通過
pmap
來檢視下程序占用的記憶體情況
pmap -x pid | sort -rn -k3 | head -30
,這段意思是檢視對應pid倒序前30大的記憶體段。這邊可以再一段時間後再跑一次指令看看記憶體增長情況,或者和正常機器比較可疑的記憶體段在哪裡。
我們如果确定有可疑的記憶體端,需要通過
gdb
來分析
gdb --batch --pid {pid} -ex "dump memory filename.dump {記憶體起始位址} {記憶體起始位址+記憶體塊大小}"
排查問題
擷取dump檔案後可用heaxdump進行檢視
hexdump -C filename | less
,不過大多數看到的都是二進制亂碼。
NMT是Java7U40引入的HotSpot新特性,配合jcmd指令我們就可以看到具體記憶體組成了。需要在啟動參數中加入
-XX:NativeMemoryTracking=summary
或者
-XX:NativeMemoryTracking=detail
,會有略微性能損耗。
一般對于堆外記憶體緩慢增長直到爆炸的情況來說,可以先設一個基線
jcmd pid VM.native_memory baseline
。
然後等放一段時間後再去看看記憶體增長的情況,通過
jcmd pid VM.native_memory detail.diff(summary.diff)
做一下summary或者detail級别的diff。
可以看到jcmd分析出來的記憶體十分詳細,包括堆内、線程以及gc(是以上述其他記憶體異常其實都可以用nmt來分析),這邊堆外記憶體我們重點關注Internal的記憶體增長,如果增長十分明顯的話那就是有問題了。
detail級别的話還會有具體記憶體段的增長情況,如下圖
此外在系統層面,我們還可以使用strace指令來監控記憶體配置設定
strace -f -e "brk,mmap,munmap" -p pid
這邊記憶體配置設定資訊主要包括了pid和記憶體位址
不過其實上面那些操作也很難定位到具體的問題點,關鍵還是要看錯誤日志棧,找到可疑的對象,搞清楚它的回收機制,然後去分析對應的對象。
- 比如DirectByteBuffer配置設定記憶體的話,是需要full GC或者手動system.gc來進行回收的(是以最好不要使用-XX:+DisableExplicitGC)。
那麼其實我們可以跟蹤一下DirectByteBuffer對象的記憶體情況,通過jmap -histo:live pid手動觸發fullGC來看看堆外記憶體有沒有被回收。
如果被回收了,那麼大機率是堆外記憶體本身配置設定的太小了,通過 -XX:MaxDirectMemorySize
進行調整。如果沒有什麼變化,那就要使用jmap去分析那些不能被gc的對象,以及和DirectByteBuffer之間的引用關系了。
最後:
我想,可能還有很多人在今年剛過去的金三銀四春招中保持着觀望的形勢,害怕自己的能力不夠,或者是安于現狀,覺得目前拿着幾千的月薪覺得能夠接受,那麼你就要注意了,這是非常危險的!
我們身為技術人員,最怕的就是安于現狀,一直在原地踏步,那麼你可能在30歲就會迎來自己的職業危機,因為你工作這麼久提升的隻有自己的年齡,技術還是萬年不變!
如果你想在未來能夠自我突破,圓夢大廠,那或許以上這份Java學習資料,你需要閱讀閱讀,希望能夠對你的職業發展有所幫助。
擷取方式: 隻需你**點贊+關注**後,加入Java架構資源交流群,找管理者擷取哦-!
為你工作這麼久提升的隻有自己的年齡,技術還是萬年不變!
如果你想在未來能夠自我突破,圓夢大廠,那或許以上這份Java學習資料,你需要閱讀閱讀,希望能夠對你的職業發展有所幫助。
擷取方式: 隻需你**點贊+關注**後,加入Java架構資源交流群,找管理者擷取哦-!