以下是Java應用在運行時常見的一些問題,總結了運行時黑盒方式的一些排查方法,也希望看到的同學能給予補充,無論是補充碰到的問題,還是補充解決方法。
類裝載的相關問題
寫過Java代碼的同學估計都碰到過ClassNotFoundException/NoClassDefFoundError/NoSuchMethodException(還有一個常見的ClassCastException就不在這裡說了)。
當碰到ClassNotFoundException/NoClassDefFound時,如果很確定這個class應該是從哪個路徑裝載的,則可以去相應的路徑找下是否有對應的class文件存在,例如web應用通常會在*.war(ear)/WEB-INF/lib或classes目錄下,對於lib下的jar包,可通過寫個小腳本jar -tvf的方式找找;
如不確定class是從哪裝載的,則可以先看看日志裡是否有堆棧信息,如果有的話則可以看到具體是哪個ClassLoader實現在裝載class,之後則可以通過www.grepcode.com或jar包反編譯(推薦一個挺好用的反編譯工具)看看具體是從哪裝載的class;
如日志中沒有,則可以用btrace來跟蹤下拋出以上兩個異常的堆棧信息,btrace腳本類似如下:
import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.*;
@BTrace public class Trace{
@OnMethod(
clazz="java.lang.ClassNotFoundException",
method="<init>"
)
public static void traceExecute(){
jstack();
}
}
拿到堆棧信息後,可以繼續使用上面的方法進行排查,在確認了class裝載的位置後,則可將相應的class/jar加上即可。
這裡還有個NoClassDefFoundError排查的case,感興趣的話可以看看。
當碰到NoSuchMethodException時,通常是由於不存在需要的class版本或class版本沖突造成的,在這種情況下,可通過在啟動參數上增加-XX:+TraceClassLoading,重啟後在日志裡看看此class是在哪load的,然後可以在對應的路徑下用jar -tvf找找是不是有正確的版本的jar存在,通常可能會發現是版本沖突造成的,對於版本沖突的問題通常需要刪掉有沖突的版本的jar,對於沒有正確版本的,則需要用正確版本的jar替換掉(當然,這種通常還會出現一些惡心的問題,例如和容器/框架的jar沖突等)。
cpu us消耗高
當出現cpu us消耗高時,通常的排查方法如下。
從經驗上來說,有些時候是由於頻繁cms gc或fgc造成的(頻繁的意思是差不多每次cms gc或fgc一結束後又立刻繼續),在gc log是記錄的情況下(-Xloggc:),可通過gc log看看,如果沒打開gc log,可通過jstat -gcutil來查看,如是gc頻繁造成的,則可跳到後面的內存問題 | GC頻繁部分看排查方法。
如不是上面的原因,可使用top -H查看線程的cpu消耗狀況,這裡有可能會看到有個別線程是cpu消耗的主體,這種情況通常會比較好解決,可根據top看到的線程id進行十六進制的轉換,用轉換出來的值和jstack出來的java線程堆棧的nid=0x[十六進制的線程id]進行關聯,即可看到此線程到底在做什麼動作,這個時候需要進一步的去排查到底是什麼原因造成的,例如有可能是正則計算,有可能是很深的遞歸或循環,也有可能是錯誤的在並發場景使用HashMap等,例如這裡還有一段隨即生成字符串的耗cpu的代碼case。
如top -H看到的消耗cpu的線程是不斷變化的,就比較麻煩了,有個同學寫了個腳本自動的去通過top -H看到的消耗cpu的線程找到對應的Java線程堆棧,在這種情況下可以用這個腳本去試試,如果看到的線程堆棧確實是比較耗cpu的動作,則基本可以定位到。
如仍然看不出,則可以嘗試多jstack看看,然後多看看是否經常有一些耗cpu的動作在不同的線程不斷的出現。
如可使用perf,則可用perf top看看cpu消耗的熱點,不過默認的版本上只能看到jit後的代碼,因此可能會比較難對應到具體的代碼,這裡有一個基於perf排查的Java應用cpu us詭異現象的case。
總結來說,cpu us消耗高的問題排查還是有一定復雜性,例如之前我碰到過反序列化的對象比較大,請求又非常頻繁,導致cpu us消耗增高了很多,但當時的機器內核版本不夠,不支持perf,從jstack等等上都看不出什麼,後來是由於從業務監控的變化上才排查出問題。
cpu iowait高
具體可見一個cpu iowait高的case的排查。
cpu sy高
具體可見這個case的排查。
內存問題
盡管JVM是自動管理內存的分配和回收的,但Java程序員們還是會經常碰到各種各樣的內存問題。
最常見的第一個問題是java.lang.OutOfMemoryError,估計寫Java的同學都碰到過。
在日志中可能會看到java.lang.OutOfMemoryError: Unable to create new native thread,可以先統計下目前的線程數(例如ps -eLf | grep java -c),然後可以看看ulimit -u的限制值是多少,如線程數已經達到限制值,如限制值可調整,則可通過調整限制值來解決;如不能調限制值,或者創建的線程已經很多了,那就需要看看線程都是哪裡創建出來的,同樣可通過btrace來查出是哪裡創建的,腳本類似如下:
import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.*;
@BTrace public class Trace{
@OnMethod(
clazz="java.lang.Thread",
method="start"
)
public static void traceExecute(){
jstack();
}
}
在找到是哪裡創建造成了後,之後就可以想辦法解決了,例如這種情況下常見的有可能是用了Executors.newCachedThreadPool這種來創建了一個沒限制大小的線程池。
還有一種可能是ulimit -u的限制還沒到,內存也空閒,但仍然創建不了,這有可能是由於在2.6.18/32內核上kernel.pid_max默認的32768造成的,這個值其實直接限制了最多能創建的線程數就是32768(即使ulimit -u的值比這大也沒用)。
java.lang.OutOfMemoryError: Heap Size或GC overhead limit exceeded也是常見的現象,在出現了這兩種現象的情況下,最重要的是dump出內存,一種方法是通過在啟動參數上增加-XX:+HeapDumpOnOutOfMemoryError,另一種方法是在當出現OOM時,通過jmap -dump獲取到內存dump,在獲取到內存dump文件後,可通過MAT進行分析,但通常來說僅僅靠MAT可能還不能直接定位到具體應用代碼中哪個部分造成的問題,例如MAT有可能看到是某個線程創建了很大的ArrayList,但這樣是不足以解決問題的,所以通常還需要借助btrace來定位到具體的代碼,可以看看這兩個OOM排查的case。