今天花了一整天在跟蹤一個問題,每次感覺已經快找到原因的時候發現現象又變了,我覺得從中吸取的教訓可以給大家分享一下。
為了重制這個現象,我寫了一個簡單的例子。在本例中,先初始化了一個map,然後用一個無限循環将一些鍵值對插入到map裡面:
class Wrapper {
public static void main(String args[]) throws Exception {
Map map = System.getProperties();
Random r = new Random();
while (true) {
map.put(r.nextInt(), "value");
}
你可能也猜到了,這段代碼編譯執行後無法正常結束。當我用這組參數啟動的話:
java -Xmx100m -XX:+UseParallelGC Wrapper
我會在終端中看到java.lang.OutOfMemoryError: GC overhead limit exceeded的異常資訊。不過如果我調整一下堆大小或者是GC的類型的話,在我的Mac OS X 10.9.2 系統上用
OracleHotspot JDK 1.7.0_45來運作,就會出現不同的情況。
比如說,我用一個較小的堆來運作這個程式,就像下面這樣:
java -Xmx10m -XX:+UseParallelGC Wrapper
應用程式會抛出一段大家更熟悉的錯誤資訊然後挂掉:java.lang.OutOfMemoryError:
Javaheap space.
如果你換成ParallelGC以外的GC政策的話,比如說-XX:+UseConcMarkSweepGC or -XX:+UseG1GC,你将會看到由預設的異常處理器所抛出的異常,并且你看不到堆棧資訊了,因為堆已經沒有空間了,甚至連異常的堆棧資訊都沒法填充了,是以它在建立異常的時候就挂掉了:
My Precious:examples vladimir$ java -Xmx100m -XX:+UseConcMarkSweepGC Wrapper
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
這說明了什麼?當資源緊缺的時候,你根本沒法判斷你的應用程式是怎麼挂掉的,是以不要指望能出現你所預期的一系列錯誤提示。從上面這個例子中可以看到,你的程式可能會以三種完全不同的方式挂掉:
GC的
安全性檢查失敗:一旦GC花費的時間占到98%以上的話,JVM就會宣告投降了: java.lang.OutOfMemoryError: GC overhead limit exceeded.
無法為下一個操作配置設定足夠的記憶體:如果無法滿足下一條指令所需要配置設定的記憶體的話,你會收到一條"java.lang.OutOfMemoryError:
heap space" 的錯誤資訊。
你可能也總結出來了,還有一種情況是你的記憶體已經緊張到連JVM建立一條OutOfMemoryError異常,填充堆棧資訊,列印到螢幕上這點要求都滿足不了了。這種情況UncaughtExceptionHandler會捕獲到這個錯誤,而不再走通常的錯誤流程。這個處理器恰如其名,當線程由于某個異常快要挂掉的時候,它開始出來收場了。出現這種情況的話,JVM會找到線程對應的 UncaughtExceptionHandler,然後調用它的uncaughtException方法。
是以當你捕獲到記憶體不足的異常并自以為已經胸有成竹時,請再多思考一下 .系統已經處于崩潰的邊緣,你原認為你能依賴的資訊很可能會消失或者改變。留給你的隻有一臉茫然,正如我前面那12個小時中那樣。
如果你已經耐心讀到這了,我推薦你關注下我們的twitter帳号。我們每周都會發一些工作中碰到的一些性能調優的問題。