2014年4月25日 星期五

死法無法預測

原文網址:https://plumbr.eu/blog/you-cannot-predict-the-way-you-die


在花了一天對付另一個 Heisenbug:每當我快抓到原因,它就會變了樣;我想我在這個 case 中學到的東西應該有分享的價值。

我寫了一個簡單的範例來展示這個狀況。在這個例子中,我建立一個 Map 然後用無窮迴圈往裡頭狂塞 key-value:

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");
    }
  }
}

你可能也看得出來,compile 然後執行這段程式碼是不會有什麼好下場。正確來說,當用下面的指令執行時:

java -Xmx100m -XX:+UseParallelGC Wrapper

shell 就會出現 java.lang.OutOfMemoryError: GC overhead limit exceeded。但如果用不同的 heap 大小、或是不同 GC,我的 Mac OS X 10.9.2 + Oracle Hotspot JDK 1.7.0_45 會選擇不同的死法。

例如設定比較小的 heap 來執行,如下:

java -Xmx10m -XX:+UseParallelGC Wrapper

application 會用比較熟悉的死法,也就是在 Map 調整大小時炸 java -Xmx100m -XX:+UseParallelGC Wrapper

用 ParallelGC 以外的 GC 演算法,像是 -XX:+UseConcMarkSweepGC-XX:+UseG1GC,炸出來的錯誤訊息是預設的 exception handler 抓到的,因為 heap 已經耗盡,所以在 Exception 建立時甚至無法設定 stacktrace、也就不會有 stacktrace:

My Precious:examples vladimir$ java -Xmx100m -XX:+UseConcMarkSweepGC Wrapper
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

這個故事的教訓是:你無法選擇你的 application 在資源不足的時候會以哪一種方法掛掉,所以也無法用一系列特定的行為來推測。在上頭的例子就可以看到有三種完全不同的失敗方式:

  1. GC 內建的安全檢查失敗:當 GC 花超過 98% 的時間在 GC 上但是沒啥效果(heap 清出的空間少於 2%),JVM 會放棄然後炸 java.lang.OutOfMemoryError: GC overhead limit exceeded
  2. 下一個操作無法取得更多記憶體:每當下一個指令嘗試要求比現在 heap 可用空間還大的記憶體,就會炸 java.lang.OutOfMemoryError: Java heap space
  3. 你可能已經製造過這個狀況,當記憶體用完、JVM 無法建立一個新的 OutOfMemoryError instance、也無法填 stacktrace 內容並把它送到 print stream 輸出。如此一來錯誤會是 UncaughtExceptionHaneler 炸出來的,而且不走正規的控制流程。這個 handler 人如其名,在 thread 因為 uncaught exception 而終止時會發揮作用。在這類案例中,JVM 會用它的 UncaughtExceptionHandler 查詢 thread、然後呼叫 handler 的 uncaughtException()

所以每當你覺得抓到表示缺乏資源的錯誤時,再想一下。系統可能處在一個脆弱的狀態,你覺得你可以倚賴的徵狀會改變或是消失。然後過了 12 個小時,只會讓你跟我一樣眼花撩亂不知所措。

沒有留言:

張貼留言