Linux下JVM常見問題處理
一、背景分析
線上故障主要會包括 CPU、記憶體、磁碟以及網路問題,而大多數故障可能會包含不止一個層面的問題,所以進行排查時候儘量四個方面依次排查一遍。基本上出問題就是 df、free、top,然後依次 使用jstack、jmap,具體問題具體分析。
二、CPU分析
一般來講我們首先會排查 CPU 方面的問題。原因包括業務邏輯問題(死迴圈)、頻繁 GC以及上下文切換過多。而最常見的往往是業務邏輯(或者框架邏輯)導致的,可以使用 jstack 來分析對應的執行緒棧情況。
1.使用 jstack 分析執行緒棧
先用 ps 命令找到對應程序的 pid,有好幾個目標程序,可以先 top 找到cpu較高的程序。
接著top -H -p <pid> 找到 CPU 使用率比較高的一些執行緒
然後將佔用最高的 pid 轉換為 16 進位制printf '%x\n' <pid>得到 nid
接著直接在 jstack 中找到相應的堆疊資訊
jstack <pid>|grep '<nid>' -C5 –color |
利用jstack生成虛擬機器中所有執行緒的快照
jstack -l {pid} > {path} cat {path}|grep '790D' -C 5 |
執行緒快照格式都是統一的,我們以一個執行緒快照簡單說明下
"http-nio-9001-exec-269" #536 daemon prio=5 os_prio=0 tid=0x00007f04700eb800 nid=0x790d runnable [0x00007f0435cb4000] java.lang.Thread.State: RUNNABLE "http-nio-9001-exec-269"執行緒名稱 #536執行緒編號 daemon守護執行緒 prio=5執行緒的優先順序 os_prio=0系統級別的執行緒優先順序 tid=0x00007f04700eb800執行緒id nid=0x790d native執行緒的id runnable [0x00007f0435cb4000]執行緒當前狀態 |
對整個 jstack 檔案進行分析,通常我們會比較關注 WAITING 和 TIMED_WAITING 的部分,如果 WAITING 之類的特別多,那麼多半是有問題。
cat jstack.log | grep "java.lang.Thread.State" | sort -nr | uniq -c |
2.頻繁 GC
先確定下 gc 是不是太頻繁,使用jstat -gc pid 1000命令來對 gc 分代變化情況進行觀察,1000 表示取樣間隔(ms)
jstat -gc pid 1000 |
如果看到 gc 比較頻繁,再針對 gc 方面做進一步分析。
3.上下文切換
針對頻繁上下文問題,我們可以使用vmstat命令來進行檢視,cs(context switch)一列則代表了上下文切換的次數。
如果對特定的 pid 進行監控可以使用 pidstat -w pid命令,cswch 和 nvcswch 表示自願及非自願切換。
pidstat -w <pid> |
4.分析過程
(1)找到CPU使用率最高的程序
(2)找到該程序中CPU使用率最高的執行緒
(3)利用jstack生成該程序下所有執行緒快照
(4)在快照中找到對應的執行緒分析問題
三、磁碟
首先是磁碟空間方面,使用df -hl來檢視檔案系統狀態
df -hl |
更多時候磁碟問題還是效能上的問題,可以通過 iostat -d -k -x來進行分析
iostat -d -k -x |
最後一列%util可以看到每塊磁碟寫入的程度,而rrqpm/s以及wrqm/s分別表示讀寫速度,一般就能定位到具體哪塊磁碟出現問題了。
四、記憶體
主要包括 OOM、GC 問題和堆外記憶體。一般來先用free命令先來檢查記憶體的各種情況。
free 或者 free -h |
1.堆內記憶體
記憶體問題大多還都是堆內記憶體問題。表象上主要分為 OOM 和 Stack Overflow。
(1)OOM
JMV 中的記憶體不足,OOM 大致可以分為以下幾種:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
沒有足夠的記憶體空間給執行緒分配 Java 棧,基本上還是執行緒池程式碼寫的有問題,比如說忘記 shutdown,JVM 方面可以通過指定Xss來減少單個 thread stack 的大小。另外也可以在系統層面增大 os 對執行緒的限制。
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
堆的記憶體佔用已經達到-Xmx 設定的最大值。解決思路仍然是先應該在程式碼中找,懷疑存在記憶體洩漏,通過 jstack 和 jmap 去定位問題。如果一切都正常需要通過調整Xmx的值來擴大記憶體。
Caused by: java.lang.OutOfMemoryError: Meta space
元資料區的記憶體佔用已經達到XX:MaxMetaspaceSize設定的最大值,解決思路仍然是先應該在程式碼中找,懷疑存在記憶體洩漏,通過 jstack 和 jmap 去定位問題。引數方面可以通過XX:MaxPermSize來進行調整。
java.lang.StackOverflowError
棧溢位丟擲java.lang.StackOverflowError錯誤,出現此種情況是因為方法執行的時候,請求新建棧幀時,棧所剩空間小於戰幀所需空間(執行緒棧需要的記憶體大於 Xss 值)。常見於通過深度遞迴呼叫方法,不停的產生棧幀,一直把棧空間堆滿,直到丟擲異常。
Exception in thread "main" java.lang.StackOverflowError
(2)使用 JMAP 定位程式碼記憶體洩漏
關於 OOM 和 Stack Overflo 的程式碼排查方面,我們一般使用 JMAP來匯出 dump 檔案。
jmap -dump:format=b,file=filename <pid> |
MAT(Eclipse Memory Analysis Tools)
mat獨立版下載地址http://www.eclipse.org/mat/downloads.php
注:安裝完成後,為了更有效率的使用MAT,可以配置一些環境引數。因為通常而言,分析一個堆轉儲檔案需要消耗很多的堆空間,為了保證分析的效率和效能,建議分配給MAT儘可能多的記憶體資源。可以採用如下兩種方式來分配記憶體更多的記憶體資源給MAT。
第一修改啟動引數MemoryAnalyzer.exe-vmargs -Xmx4g
第二編輯檔案MemoryAnalyzer.ini,在裡面新增類似資訊-vmargs– Xmx4g。
1. MemoryAnalyzer.ini中的引數一般預設為-vmargs– Xmx1024m,這就夠用了。假如機器的記憶體不大,改大該引數的值,會導致MemoryAnalyzer啟動時,報錯:Failed to create the Java Virtual Machine。
2.當匯出的dump檔案的大小大於配置的1024m(說明1中,提到的配置:-vmargs– Xmx1024m),MAT輸出分析報告的時候,會報錯:An internal error occurred during: "Parsing heap dump from XXX”。適當調大說明1中的引數即可。
mat(Eclipse Memory Analysis Tools)匯入 dump 檔案進行分析。
記憶體洩漏問題一般直接選 Leak Suspects 即可,mat 給出了記憶體洩漏的建議。執行緒相關的問題可以選擇 thread overview 進行分析。
IBM記憶體分析工具 ha457.jar
IBM HeapAnalyzer下載地址https://www.ibm.com/support/pages/ibm-heapanalyzer
生產環境自動dump出oom資訊的phrof檔案
使用啟動引數 -XX:+HeapDumpOnOutOfMemoryError
匯出地址:-XX:+HeapDumpPath={path}
2.GC 問題和執行緒
GC問題除了影響 CPU 也會影響記憶體,排查思路也是一致的。一般先使用 jstat 來檢視分代變化情況,比如 youngGC 或者 fullGC 次數是不是太多;EU、OU 等指標增長是不是異常等。
執行緒的話太多而且不被及時 gc 也會引發 oom,大部分是unable to create new native thread。
3.堆外記憶體
堆外記憶體溢位表現就是物理常駐記憶體增長快,報錯的話視使用方式都不確定,如果由於使用 Netty 導致的,那錯誤日誌裡可能會出現OutOfDirectMemoryError錯誤,如果直接是 DirectByteBuffer,那會報OutOfMemoryError: Direct buffer memory。
堆外記憶體溢位往往是和 NIO 的使用相關,一般我們先通過 pmap 來檢視下程序佔用的記憶體情況pmap -x pid | sort -rn -k3 | head -30,意思是檢視對應 pid 倒序前 30 大的記憶體段。這邊可以再一段時間後再跑一次命令看看記憶體增長情況,或者和正常機器比較可疑的記憶體段在哪裡。
關鍵還是要看錯誤日誌棧,找到可疑的物件,搞清楚回收機制,然後去分析對應的物件。比如 DirectByteBuffer 分配記憶體的話,是需要 full GC 或者手動 system.gc 來進行回收的。那麼其實我們可以跟蹤一下 DirectByteBuffer 物件的記憶體情況,通過jmap -histo:live pid手動觸發 fullGC 來看看堆外記憶體有沒有被回收。如果被回收了,那麼大概率是堆外記憶體本身分配的太小了,通過-XX:MaxDirectMemorySize進行調整。如果沒有什麼變化,那就要使用 jmap 去分析那些不能被 gc 的物件,以及和 DirectByteBuffer 之間的引用關係了。
五、GC 問題
堆內記憶體洩漏總是和 GC 異常相伴。不過 GC 問題不只是和記憶體問題相關,還有可能引起 CPU 負載、網路問題等,只是相對來說和記憶體聯絡緊密些。
在啟動引數中加上-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps來開啟 GC 日誌。GC日誌能大致推斷出 youngGC 與 fullGC 是否過於頻繁或者耗時過長。
1.youngGC 過頻繁
youngGC 頻繁一般是短週期小物件較多,先考慮是不是新生代設定的太小了,看能否通過調整-Xmn、-XX:SurvivorRatio 等引數設定來解決問題。如果引數正常,但是 young gc 頻率還是太高,就需要使用 Jmap 和 MAT 對 dump 檔案進行進一步排查了。
-Xmn2G設定年輕代大小為2G -XX:SurvivorRatio=8 定義了新生代中Eden區域和Survivor區域的比例,預設為8,也就是說Eden佔新生代的8/10,From倖存區和To倖存區各佔新生代的1/10 |
2.youngGC 耗時過長
耗時過長問題就要看 GC 日誌裡耗時耗在哪一塊了。
3.觸發 fullGC
(1)直接呼叫 System.gc() 時(呼叫後並不會立即發生 fullGC,後面會在某個時間點發生)
(2)老年代的可用空間不足時
(3)方法區空間不足時,或 Metaspace Space 使用達到 MetaspaceSize 但未達到 MaxMetaspaceSize 閾值;大多情況下擴容都會觸發
(4)通過Minor GC後進入老年代的平均大小大於老年代的可用記憶體時。由 Eden 區、From Survior 區向 To Survior 區複製時,物件大小大於 To Survior 區可用記憶體,則把該物件轉存到老年代
(5)執行 jmap -histo:live 或者 jmap -dump:live;
分析過程
(1)找到記憶體使用率最高的程序
(2)利用jmap生成該程序的堆轉儲快照
(3)利用轉儲快照分析工具分析問題
六、總結
1.JVM 常用命令
jps:列出正在執行的虛擬機器程序
jstat:監視虛擬機器各種執行狀態資訊,可以顯示虛擬機器程序中的類裝載、記憶體、垃圾收集、JIT編譯等執行資料
jinfo:實時檢視和調整虛擬機器各項引數
jmap:生成堆轉儲快照,也可以查詢 finalize 執行佇列、Java 堆和永久代的詳細資訊
jstack:生成虛擬機器當前時刻的執行緒快照
jhat:虛擬機器堆轉儲快照分析工具
與 jmap 搭配使用,分析 jmap 生成的堆轉儲快照,與 MAT 的作用類似
2.排查步驟
1、先找到對應的程序:PID
2、生成執行緒快照stack(或堆轉儲快照:hprof)
3、分析快照(或堆轉儲快照),定位問題
3.記憶體洩露、記憶體溢位和 CPU 100% 關係
4.常用 JVM 效能檢測工具
Eclipse Memory Analyer、JProfile、JProbe Profiler、JVisualVM、JConsole、Plumbr
沒有高深的知識,沒有進階的技巧,萬丈高樓平地起~!