【總結】常見Java故障案例
原文 http://dbaplus.cn/news-21-173-1.html
目錄
-
HotSpot常識
-
Java故障排查方法論
-
Java故障案例分析
Part 1
HotSpot常識
-
HotSpot是目前最常見的開源JVM(GPL協議),用來執行Java應用和applet,本次討論基本都是基於這一軟體來進行的。
-
所有的Java物件都是分配在Java堆上的,Java程式碼中看到的引用,在JVM的實現中就是一個指標,指向一段被表示成物件的記憶體區域。這個區域可能被移動,引用指標的值不同於一般的C/C++指標,是會從外部改變的。
-
執行的Java位元組碼都是動態載入、連結、編譯的。
-
JIT compiler,JVM裡面有一個模組負責把Java位元組碼編譯成優化過的native機器碼,這樣可以極大提高執行效率。
但 是HotSpot的JIT編譯器只會編譯熱點方法,一個Java方法load進來後會預設從直譯器開始執行,只有部分或整體的解釋執行次數超過一定次數才 會被編譯優化,在某些條件下,比如debug,會把方法去優化退回到直譯器執行。直譯器可以看做是一個沒有優化的翻譯器,會把每一條bytecode指令 機械的翻譯成彙編指令來執行。
1.6, 兩個stack,interned string放到heap
這張圖裡每一個小方塊展開都可以寫一系列文章,今天就不在這裡展開了。
參考連結:
http://blog.jamesdbloom.com/images_2013_11_17_17_56/JVM_Internal_Architecture.png
http://blog.jamesdbloom.com/JVMInternals.html
Part 2
Java故障排查方法論
1 1參考書
2 幾個我個人常用的三個原則
-
從淺顯和廣泛開始。分析問題應該儘量從高層入手,收集各種各樣的現場資訊,版本資訊,儘量不要一開始就debugger跑起。
-
分而治之,隔離問題。將問題隔離到儘可能小的領域中,比如某個特定系統、特定版本、 甚至特定機器中。之後如果是java的問題,還可以繼續分析是java應用、容器、或者jdk的問題,最後應該能確定到某個模組的某些程式碼、一次 commit、一行配置的問題。整個排查問題的過程就是一個從上到下,一步步縮小問題範圍的過程。
-
福爾摩斯法則。當排除了所有的不可能,那麼剩下的那個,不管多麼荒謬,就是罪魁禍首了。
3 重現故障和收集資料
不同於其他工業系統,軟體工業的一個好處就是重新嘗試的代價一般都特別小,重啟一個程序總比重啟一臺發動機、一個核反應堆輕鬆很多。所以如果故障問題能穩定的通過重啟復現,這對於修bug的同學將會是個天大的好訊息。
但是現實中,特別是在生產環境中,更多的事後故障問題不是你想發現就能發現,經常是重啟後就沒了,跑了不確定的時間就又出現了,所以只能通過收 集故障時的系統狀態資料來分析問題。狀態資料大致可以分為兩類:一是監控類資料,收集這類資料對於應用的效能影響很小,基本可以忽略不計,所以可以持續收 集,比如GC log,應用log等;第二類是某些瞬時資料,這些資料要麼收集的代價很大,很影響系統性能,要麼時效性很高,過了故障點一切可能就都不一樣了,所以不能 持續收集,必須迅速的在故障出現點自動採集,比如Heap dump,core dump等。
下面這個圖描述了常見的Java故障和需要收集的資料之間的概要關係
JVM級別資料
對於JVM,下面這些選項最好常年開啟選項,對於收集故障資料很有幫助
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/log/gcdump
系統級別數據
Java程序執行的環境資訊也是重要的診斷資訊,如果能在故障點全部收集下來對於後續除錯分析也是很有幫助的,這些資訊主要包括: 系統基本軟硬體資訊、所有程序的情況、開啟的檔案描述符等等。
簡單的做法可以在Java程序非正常返回的時候執行一個指令碼,自動的去採集一遍這些資訊。(HotSpot支援在致命錯誤或者oom時執行一個系 統命令,可以設定讓其去直接執行這個指令碼)。或者說是使用一個監控程式,監視Java程序的輸出結果,如果發現異常、crash等情況,就收集一次環境信 息。
Part 3
Java故障案例分析
故障1 CPU load過高問題一般是指CPU使用率很高,但是系統並沒有很繁忙,一般有兩種情形。
情況1,啟動階段
應用剛啟動之後或者剛放了使用者流量之後,也是可能突然cpu load飆到很高的,這一般不是java程式碼引起的,而是由於jvm的jit編譯器引起的。(當然如果你使用的是一些非普遍的JDK,比如IBMJDK, 並且啟用了AOT之類的功能,是不可能遇到這個情況的,因為程式碼已經提前編譯好了)
-
-XX:+TieredCompilation
可以先一定程度上減輕這個問題,效果上相當於把消耗資源嚴重的一些優化處理延後進行了,先把java方法編譯到一個低優化級別的native方 法。值得注意的是,這個引數會消耗比較多的記憶體資源,同一個方法被編譯了多次,存在多份native記憶體拷貝,建議是把codecache調大一點兒 (-XX:+ReservedCodeCacheSize,InitialCodeCacheSize)。
Optional:
CodeCache不足可能會引起效能問題,這是一種非常少見的故障,code cache不足,jit需要編譯新的方法的時候就會不停的嘗試清理code cache,丟棄掉無用的方法,頻繁的嘗試會導致大量資源消耗在JIT執行緒上。
-
-XX:+PrintCompilation
為了確認這個問題可以嘗試使用這個引數,輸出JIT編譯的情況,如果初始階段發生大量方法的編譯,就可以確定是由於JIT編譯引起的。一般情況下,忍一忍熬過一開始的編譯階段就好了。如果使用者請求超時嚴重,無法忍受,可以嘗試使用分層編譯、提前預熱系統。
情況2,非啟動階段
一般是一些計算密集型任務、忙等操作、或者過於密集的執行緒排程。一般需要定位出被頻繁執行的程式碼邏輯(熱點方法),然後再進行優化,目前可以使用 各種profile工具來分析。比如Java Mission Control, ZProfiler(硬廣:阿里自產的profiler工具)
故障2 應用效能下降/較差
這個問題又兩個層面,一個是應用的效能下降了,這一般是來自監控系統或者使用者突然的報警 。從分割問題的角度看,效能下降一般是和之前時間點比較得出的結論,那麼就肯定有一個分水嶺,在某一個時間點(通常是一個改動發生的時候)之後就會開始性 能下降。所以初始的解決方案比較簡單,就是找到改動發生的時間點,挑出造成效能下降的改動,然後分析這個改動為什麼會造成效能下降。
但是如果就是一個應用效能較差的問題,就比較棘手了,這個通常意味著沒有可以比較的時間點,相當於憑空設定一個性能指標,將系統性能優化提升到這 個目標。通常這是一個需要多方合作,修改多個層次的程式碼、配置才能達到的目標。通常而言可以繼續嘗試profiling Java應用,分析效能瓶頸,優化瓶頸部分。
可能有影響的瓶頸包括:
鎖
這個一般需要設計、程式碼層面的改動,使用更高效的加鎖機制,減輕競爭,等等。
GC
頻繁full gc的又有兩種情況,一種是說full gc完了之後整個heap還是沒有很多的可用空間,一般是可能是由於最大heap上限可能設定有點兒小了,或者應用有記憶體洩露,需要做個heap dump具體分析下記憶體裡面各個部分的使用情況。
另外一個情況是full gc完了之後整個heap還是有不少的可用空間的,比如下圖,這個一般是有一些“臨時”物件晉升到了老年代,新生代沒有濾掉足夠的短生命週期物件,可能需 要調整JVM引數-XX:MaxTenuringThreshold(15, 4bits)提高promote到老年代的門檻。
分析GC日誌,一個開源的免費解決方案是eclipse的GCMV
GC引數優化
關於GC其實你能做的並不多,影響最大就是通過調整JVM啟動時引數,來調節GC的各個行為,但是推薦讀懂了官方文件中的說明再做調整:
http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html
1. 仔細設計一個適合你自己環境、應用的引數模板。
2. 收集應用資訊,評估應用記憶體活動行為(參見“Java效能優化權威指南”),常駐記憶體物件大小,大物件比例,native記憶體使用,分配速度等。。。
3. 調整下列引數(不是一條命令哦)
-Xms8888m
-Xmx8888m
-Xmn8888m
-Xss8888k
-XX:PermSize=8888m
-XX:MaxPermSize=8888m
-XX:+UseStringCache
-XX:+UseConcMarkSweepGC
-XX:+UseG1GC
-XX:+UseParNewGC
-XX:ParallelGCThreads=8888
-XX:+CMSClassUnloadingEnabled
-XX:+DisableExplicitGC
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=88
GC停頓時間太長
堆太大的時候,CMS GC可能會停頓比較久的時間,-XX:+CMSScavengeBeforeRemark能通過在remark階段前做一次young gc減輕這個時間。
另外可以考慮換G1。
故障3 記憶體耗盡OOM
基本的解決思路,多給點兒或者少用點兒唄。
-
Java物件真的耗盡了記憶體資源,Eclipse MAT
HeapDump,分析記憶體洩露,大物件,物件關係圖。 -
Native記憶體耗盡, DirectBuffer,malloc
JVM執行過程中,雖然會對Java堆做垃圾收集,但是如果jni或者非DirectBuffer的Unsafe分配的記憶體沒有回收,會逐漸累積直至java程序結束。DirectBuffer雖然Java物件很小,但是使用的記憶體可能會很多。
參考:http://lovestblog.cn/blog/2015/05/12/direct-buffer/
-
PermGen耗盡,一般動態類載入導致,已經成為歷史,儘早升級吧。
故障4 崩潰crash
現代JVM發展到今天已經很健壯了,一般很少會出現crash的情況,如果出現了,很有可能是Java程式碼執行了不安全的操作,比如使用Unsafe去直接操作記憶體、自己編寫了JNI函式中crash了。
目前的現實是很多第三方的庫確實直接使用了Unsafe去實現各種“高效”的操作,隨便搜尋下Github就可以看到大量的開源Java、Scala庫使用了JDK提供的unsafe類
對於crash的情形,需要收集的資訊包括各種dump,最關鍵的是系統core dump,方便將來使用GDB做事後分析,在linux上一般需要使用ulimit –c unlimited 命令修改core檔案尺寸上限才行。
有了core dump,剩下的分析一般都是使用GDB繼續了,crash的情形一般反而比較直觀。如果不是unsafe、自己jni引起的crash問題,恭喜你,真的發現bug了,這個問題直接給Oracle或者java社群報bug吧。
指令碼太複雜,怎麼知道最後跑起來的Java程序到底設定了哪些引數?
-XX:-PrintCommandLineFlags