Java 中出現 OutOfMemoryError 時關閉
1. 概述
維持應用程式處於一致狀態比保持其運作更重要。對於大多數情況來說都是如此。
在本教程中,我們將學習如何在發生OutOfMemoryError
時明確停止應用程式。在某些情況下,如果沒有正確的處理,我們可能會在不正確的狀態下繼續處理應用程式。
2. OutOfMemoryError
OutOfMemoryError
是應用程式外部的錯誤,而且是不可恢復的,至少在大多數情況下是如此。錯誤名稱表示應用程式沒有足夠的 RAM,但這並不完全正確。更準確地說,應用程式無法分配所要求的記憶體量。
在單線程應用程式中,情況非常簡單。如果我們遵循準則並且沒有捕獲OutOfMemoryError
,應用程式將終止。這是處理此錯誤的預期方法。
在某些特定情況下,捕獲OutOfMemoryError
是合理的。此外,我們還可以製定一些更具體的計劃,以便在其之後繼續進行可能是合理的。然而,在大多數情況下, OutOfMemoryError
意味著應用程式應該停止。
3. 多線程
多執行緒是大多數現代應用程式不可或缺的一部分。線程遵循拉斯維加斯關於異常的規則:線程中發生的事情保留在線程中。這並不總是正確的,但我們可以將其視為一般行為。
因此,即使在線程中最嚴重的錯誤也不會傳播到主應用程序,除非我們明確地處理它們。讓我們考慮以下記憶體洩漏的範例:
public static final Runnable MEMORY_LEAK = () -> {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(tenMegabytes());
}
};
private static byte[] tenMegabytes() {
return new byte[1024 * 1014 * 10];
}
如果我們在單獨的線程中運行此程式碼,應用程式將不會失敗:
@Test
void givenMemoryLeakCode_whenRunInsideThread_thenMainAppDoestFail() throws InterruptedException {
Thread memoryLeakThread = new Thread(MEMORY_LEAK);
memoryLeakThread.start();
memoryLeakThread.join();
}
發生這種情況是因為導致OutOfMemoryError
的所有資料都連接到了 thread。當線程死亡時, List
就會失去其垃圾收集根並且可以被收集。因此,首先導致OutOfMemoryError
資料會隨著執行緒的死亡而被刪除。
如果我們多次運行此程式碼,應用程式不會失敗:
@Test
void givenMemoryLeakCode_whenRunSeveralTimesInsideThread_thenMainAppDoestFail() throws InterruptedException {
for (int i = 0; i < 5; i++) {
Thread memoryLeakThread = new Thread(MEMORY_LEAK);
memoryLeakThread.start();
memoryLeakThread.join();
}
}
同時,垃圾收集日誌顯示以下情況:
在每個循環中,我們耗盡 6 GB 可用 RAM、終止線程、運行垃圾收集、刪除數據,然後繼續。我們得到了這個堆過山車,它沒有做任何合理的工作,但應用程式不會失敗。
同時我們可以在日誌中看到錯誤。在某些情況下,忽略OutOfMemoryError
是合理的。我們不想因為錯誤或用戶漏洞而殺死整個網路伺服器。
此外,實際應用程式中的行為可能有所不同。執行緒和其他共享資源之間可能存在互連性。因此,任何執行緒都可以拋出OutOfMemoryError
。這是一個異步異常;它們不依賴特定的線路。但是,如果主應用程式執行緒中沒有發生OutOfMemoryError
,應用程式仍將運行。
4. 終止 JVM
在某些應用程式中,執行緒會產生關鍵的工作並且應該可靠地完成它。最好停止一切,調查並解決問題。
想像一下,我們正在處理一個包含歷史銀行資料的巨大 XML 檔案。我們將區塊載入到記憶體中,進行計算,並將結果寫入光碟。這個範例可以更複雜,但主要想法是,有時,我們嚴重依賴執行緒中進程的事務性和正確性。
幸運的是,JVM 將OutOfMemoryError
視為一種特殊情況,我們可以使用以下參數在應用程式中出現OutOfMemoryError
時退出或崩潰 JVM:
-XX:+ExitOnOutOfMemoryError
-XX:+CrashOnOutOfMemoryError
如果我們使用這些參數中的任何一個運行範例,應用程式將停止。這將使我們能夠調查問題並檢查發生了什麼。
這些選項之間的差異在於-XX:+CrashOnOutOfMemoryError
會產生故障轉儲:
#
# A fatal error has been detected by the Java Runtime Environment:
#
# Internal Error (debug.cpp:368), pid=69477, tid=39939
# fatal error: OutOfMemory encountered: Java heap space
#
...
它包含我們可用於分析的資訊。為了使這個過程更容易,我們還可以進行堆轉儲來進一步調查它。有一個特殊選項可以在OutOfMemoryError
上自動執行此操作。
我們也可以為多執行緒應用程式建立線程轉儲。它沒有專門的論點。但是,我們可以使用腳本並透過OutOfMemoryError
觸發它。
如果我們想以類似的方式處理其他異常,我們必須使用Futures
來確保執行緒能如預期完成其工作。將異常包裝到OutOfMemoryError
中以避免實現正確的線程間通訊是一個糟糕的主意:
@Test
void givenBadExample_whenUseItInProductionCode_thenQuestionedByEmployerAndProbablyFired()
throws InterruptedException {
Thread npeThread = new Thread(() -> {
String nullString = null;
try {
nullString.isEmpty();
} catch (NullPointerException e) {
throw new OutOfMemoryError(e.getMessage());
}
});
npeThread.start();
npeThread.join();
}
5. 結論
在本文中,我們討論了OutOfMemoryError
如何經常使應用程式處於錯誤狀態。儘管在某些情況下我們可以從中恢復,但我們應該考慮整體終止並重新啟動應用程式。
而單線程應用程式不需要對OutOfMemoryError.
多執行緒程式碼需要額外的分析和配置,以確保應用程式退出或崩潰。
與往常一樣,所有程式碼都可以在 GitHub 上取得。