聽阿里巴巴JVM工程師為你分析常見Java故障案例

努力醬發表於2017-05-02


本文根據12月23日阿里巴巴技術保障部JVM組軟體工程師陸傳勝老師的主題分享整理!小編特別整理出其中精華內容,供大家學習交流。

目錄

  • 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

 

20160712104041263.jpg

這張圖裡每一個小方塊展開都可以寫一系列文章,今天就不在這裡展開了。

 

Part 2

   

Java故障排查方法論

 

 

 

11參考書

 

 

   20160712104059183.jpg

 

  20160712104108755.jpg

 

 

2幾個我個人常用的三個原則

 

 

  • 從淺顯和廣泛開始。
    分析問題應該儘量從高層入手,收集各種各樣的現場資訊,版本資訊,儘量不要一開始就debugger跑起。

     

  • 分而治之,隔離問題。
    將問題隔離到儘可能小的領域中,比如某個特定系統、特定版本、甚至特定機器中。之後如果是java的問題,還可以繼續分析是java應用、容器、或者jdk的問題,最後應該能確定到某個模組的某些程式碼、一次commit、一行配置的問題。整個排查問題的過程就是一個從上到下,一步步縮小問題範圍的過程。

     

  • 福爾摩斯法則。
    當排除了所有的不可能,那麼剩下的那個,不管多麼荒謬,就是罪魁禍首了。

 

 

3重現故障和收集資料

 

 

不同於其他工業系統,軟體工業的一個好處就是重新嘗試的代價一般都特別小,重啟一個程式總比重啟一臺發動機、一個核反應堆輕鬆很多。所以如果故障問題能穩定的通過重啟復現,這對於修bug的同學將會是個天大的好訊息。

但是現實中,特別是在生產環境中,更多的事後故障問題不是你想發現就能發現,經常是重啟後就沒了,跑了不確定的時間就又出現了,所以只能通過收集故障時的系統狀態資料來分析問題。狀態資料大致可以分為兩類:一是監控類資料,收集這類資料對於應用的效能影響很小,基本可以忽略不計,所以可以持續收集,比如GC log,應用log等;第二類是某些瞬時資料,這些資料要麼收集的代價很大,很影響系統效能,要麼時效性很高,過了故障點一切可能就都不一樣了,所以不能持續收集,必須迅速的在故障出現點自動採集,比如Heap dump,core dump等。

 

下面這個圖描述了常見的Java故障和需要收集的資料之間的概要關係

 

20160712104116192.jpg

 

JVM級別資料

 

對於JVM,下面這些選項最好常年開啟選項,對於收集故障資料很有幫助

 

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/log/gcdump

 

系統級別資料

 

Java程式執行的環境資訊也是重要的診斷資訊,如果能在故障點全部收集下來對於後續除錯分析也是很有幫助的,這些資訊主要包括: 系統基本軟硬體資訊、所有程式的情況、開啟的檔案描述符等等。

 

簡單的做法可以在Java程式非正常返回的時候執行一個指令碼,自動的去採集一遍這些資訊。(HotSpot支援在致命錯誤或者oom時執行一個系統命令,可以設定讓其去直接執行這個指令碼)。或者說是使用一個監控程式,監視Java程式的輸出結果,如果發現異常、crash等情況,就收集一次環境資訊。

 

Part 3

   

Java故障案例分析

 

故障1CPU load過高

  

問題一般是指CPU使用率很高,但是系統並沒有很繁忙,一般有兩種情形。

 

20160712104124542.jpg

 

20160712104131987.jpg

 

情況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工具)

 

20160712104143224.jpg

20160712104150885.jpg

 

故障2應用效能下降/較差

 

 

這個問題又兩個層面,一個是應用的效能下降了,這一般是來自監控系統或者使用者突然的報警 。從分割問題的角度看,效能下降一般是和之前時間點比較得出的結論,那麼就肯定有一個分水嶺,在某一個時間點(通常是一個改動發生的時候)之後就會開始效能下降。所以初始的解決方案比較簡單,就是找到改動發生的時間點,挑出造成效能下降的改動,然後分析這個改動為什麼會造成效能下降。

 

但是如果就是一個應用效能較差的問題,就比較棘手了,這個通常意味著沒有可以比較的時間點,相當於憑空設定一個效能指標,將系統效能優化提升到這個目標。通常這是一個需要多方合作,修改多個層次的程式碼、配置才能達到的目標。通常而言可以繼續嘗試profiling Java應用,分析效能瓶頸,優化瓶頸部分。

 

可能有影響的瓶頸包括:

 

這個一般需要設計、程式碼層面的改動,使用更高效的加鎖機制,減輕競爭,等等。

 

GC

20160712104158320.jpg

頻繁full gc的又有兩種情況,一種是說full gc完了之後整個heap還是沒有很多的可用空間,一般是可能是由於最大heap上限可能設定有點兒小了,或者應用有記憶體洩露,需要做個heap dump具體分析下記憶體裡面各個部分的使用情況。

另外一個情況是full gc完了之後整個heap還是有不少的可用空間的,比如下圖,這個一般是有一些“臨時”物件晉升到了老年代,新生代沒有濾掉足夠的短生命週期物件,可能需要調整JVM引數-XX:MaxTenuringThreshold(15, 4bits)提高promote到老年代的門檻。

 

20160712104205304.jpg

20160712104213861.jpg

 

分析GC日誌,一個開源的免費解決方案是eclipse的GCMV

20160712104221299.jpg

 

GC引數優化
關於GC其實你能做的並不多,影響最大就是通過調整JVM啟動時引數,來調節GC的各個行為,
 

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。

20160712104228145.jpg

 

故障3記憶體耗盡OOM

 

基本的解決思路,多給點兒或者少用點兒唄。

  • Java物件真的耗盡了記憶體資源,Eclipse MAT
    HeapDump,分析記憶體洩露,大物件,物件關係圖。

     

  • Native記憶體耗盡, DirectBuffer,malloc

    JVM執行過程中,雖然會對Java堆做垃圾收集,但是如果jni或者非DirectBuffer的Unsafe分配的記憶體沒有回收,會逐漸累積直至java程式結束。DirectBuffer雖然Java物件很小,但是使用的記憶體可能會很多。

     

  • PermGen耗盡,一般動態類載入導致,已經成為歷史,儘早升級吧。

 

 

故障4崩潰crash

 

 

現代JVM發展到今天已經很健壯了,一般很少會出現crash的情況,如果出現了,很有可能是Java程式碼執行了不安全的操作,比如使用Unsafe去直接操作記憶體、自己編寫了JNI函式中crash了。

 

20160712104235515.jpg

 

目前的現實是很多第三方的庫確實直接使用了Unsafe去實現各種“高效”的操作,隨便搜尋下Github就可以看到大量的開源Java、Scala庫使用了JDK提供的unsafe類

 

20160712104242408.jpg

 

對於crash的情形,需要收集的資訊包括各種dump,最關鍵的是系統core dump,方便將來使用GDB做事後分析,在linux上一般需要使用ulimit –c unlimited 命令修改core檔案尺寸上限才行。

 

有了core dump,剩下的分析一般都是使用GDB繼續了,crash的情形一般反而比較直觀。如果不是unsafe、自己jni引起的crash問題,恭喜你,真的發現bug了,這個問題直接給Oracle或者java社群報bug吧。

 

20160712104249943.jpg

 

指令碼太複雜,怎麼知道最後跑起來的Java程式到底設定了哪些引數?

-XX:-PrintCommandLineFlags

 

Q & A  

Q1:關於記憶體洩露,是不是可以用一些開源產品來加探針,這方面有沒有好的建議?

A1:記憶體洩露其實有兩個方面,如果是native的malloc洩露,可以通過 valgrind,jemalloc等工具發現,這個和傳統c++應用記憶體一樣的;對於某些因為強引用鏈無法釋放的java物件,一般都是 heapdump之後使用eclipse的mat工具分析“可能洩露的”,因為程式並沒有結束,而且還持有強引用,很難得出結論說某些物件是不是記憶體洩露。

 

Q2:關於記憶體洩露, 怎麼定位到具體程式碼?

A2:定位到程式碼目前比較可行的,就是用MAT分析出那些型別的物件發生了洩露,然後去掃描程式碼看看這些物件在哪裡分配的,當然如果大量的string,byte.就不好辦了,一般是以使用者應用自定義的類為線索去查詢。這樣可以大大降低工作量。

 

講師介紹:陸傳勝

  • 現就職於阿里巴巴技術保障部JVM組,主要工作是阿里巴巴定製化JDK的開發, 以及相關的Java技術支援。

  • 曾就職於IBM Java技術中心,負責IBMJDK開發、參與OpenJDK社群,是OpenJDK jdk8專案committer。

  • 聯絡方式chuanshenglu@gmail.com


本文來自雲棲社群合作伙伴”DBAplus”,原文釋出時間:2015-12-25


相關文章