[轉帖]2024-4-23 群討論:Java堆空間OutOfMemoryError該怎麼辦

济南小老虎發表於2024-05-16
https://juejin.cn/post/7361234872780898316

以下來自本人拉的一個關於 Java 技術的討論群。關注公眾號:hashcon,私信進群拉你

1. 為什麼不建議開啟 HeapDumpOnOutOfMemoryError?

1.1. 開啟 HeapDumpOnOutOfMemoryError,哪些 OutOfMemoryError 會觸發 HeapDumpOnOutOfMemoryError?

開啟 HeapDumpOnOutOfMemoryError 之後,不是所有的 OutOfMemoryError 都會觸發 HeapDumpOnOutOfMemoryError,不同的 OutOfMemoryError 包括(如果對這些異常丟擲的原理詳情感興趣,請參考:zhuanlan.zhihu.com/p/265039643 ):

  1. OutOfMemoryError: Java heap spaceOutOfMemoryError: GC overhead limit exceeded:這兩個都是 Java 物件堆記憶體不夠了,一個是分配的時候發現剩餘空間不足,一個是到達某一界限。這兩個都會觸發 HeapDumpOnOutOfMemoryError
  2. OutOfMemoryError: unable to create native thread:無法建立新的平臺執行緒,這個不會觸發 HeapDumpOnOutOfMemoryError
  3. OutOfMemoryError: Requested array size exceeds VM limit:當申請的陣列大小超過堆記憶體限制,就會丟擲這個異常。這個會觸發 HeapDumpOnOutOfMemoryError
  4. OutOfMemoryError: Compressed class spaceOutOfMemoryError: Metaspace:這兩個都和元空間相關(底層原理說明參考:juejin.cn/post/722587… ),這兩個都會觸發 HeapDumpOnOutOfMemoryError
  5. OutOfMemoryError: Cannot reserve xxx bytes of direct buffer memory (allocated: xxx, limit: xxx):在 DirectByteBuffer 中,首先向 Bits 類申請額度,Bits 類有一個全域性的 totalCapacity 變數,記錄著全部 DirectByteBuffer 的總大小,每次申請,都先看看是否超限,可用 -XX:MaxDirectMemorySize 限制。這個不會觸發 HeapDumpOnOutOfMemoryError
  6. OutOfMemoryError: map failed:這個是 File MMAP(檔案對映記憶體)時,如果系統記憶體不足,就會丟擲這個異常。這個不會觸發 HeapDumpOnOutOfMemoryError

還有一些其他的:

  1. Shenandoah 分配區域點陣圖,記憶體的時候,觸發的 OutOfMemoryError,這個會觸發 HeapDumpOnOutOfMemoryError
  2. OutOfMemoryError: Native heap allocation failed,這個 Message 可能不同作業系統不一樣,但是一般都有 native heap。這個就和 Java 物件堆一般沒關係,而是其他塊記憶體無法申請導致的,這些不會觸發HeapDumpOnOutOfMemoryError

1.2. 為什麼不開啟 HeapDumpOnOutOfMemoryError

HeapDumpOnOutOfMemoryError 的原理:

  1. 進入安全點,所有應用執行緒暫停,針對 HeapDumpOnOutOfMemoryError,單執行緒(如果是 jcmd jmap 可以多執行緒)dump 堆為執行緒個數個檔案。退出安全點。
  2. 將上面的多個檔案,合併為一個,壓縮。

這裡的瓶頸主要在於第一步寫入,並且,主要瓶頸再磁碟 IO,我們來看下現在雲服務的磁碟 IO 標準:

  1. AWS EFS(普通儲存):docs.aws.amazon.com/efs/latest/…
  2. AWS EBS(對標 SSD):docs.aws.amazon.com/ebs/latest/…

對於一個 4G 大小的堆記憶體,如果是 EFS,對標的應該是 100G 以內的磁碟,寫入最少也需要大概 4 * 1024 / 300 = 13.65 秒(注意,這個是峰值效能),如果當時峰值效能被用完了,那麼需要:4 * 1024 / 15 = 273 秒。如果用 EBS,那麼也需要 4 * 1024 / 1000 = 4 秒。注意,這個計算的時間,是應用執行緒個完全處於安全點(即 Stop-the-world)的時間,還沒有還是沒考慮一個機器上部署多個容器例項的情況,考慮成本我們也不能堆每個微服務都使用 AWS EBS 這種(對標 SSD)。

所以,建議還是不要開啟 HeapDumpOnOutOfMemoryError

2. 不使用 HeapDumpOnOutOfMemoryError 用什麼?

2.1. 定位記憶體洩漏問題靠 JFR

我這邊定位 OutOfMemoryError 一般透過 JFR 的 Object Allocation Sample 以及 Old Object Sample 裡面的物件去定位,只有這些都定位不出來,才會考慮 Heap Dump。

2.2. 為什麼丟擲 OutOfMemoryError 的微服務最好下線重啟?

因為包括 JDK 的原始碼在內,都沒有在每一個分配記憶體的程式碼的地方考慮會出現 OutOfMemoryError,這樣會導致程式碼狀態不一致,例如 hashmap 的 rehash,如果裡面某行丟擲 OutOfMemoryError,前面更新的狀態就不對了。還有其他很多庫,就不用說了,都很少有 catch Throwable 的,大部分是 catch Exception 的。並且,在每一個分配記憶體的程式碼的地方考慮會出現 OutOfMemoryError 也是不現實的,所以為了防止 OutOfMemoryError 帶來意想不到的一致性問題,還是下線重啟比較好。

2.3. 如何實現丟擲 OutOfMemoryError 的微服務下線重啟?

一般透過 -XX:OnOutOfMemoryError="/path/to/script.sh"指定指令碼,指令碼執行:

  1. 微服務的下線
  2. 微服務的重啟

針對 spring boot,可以考慮開啟允許本地訪問 /actuator/shutdown 來關閉微服務(有群友反應丟擲 OutOfMemoryError 的時候呼叫這個會卡死,這是因為 1.2 說的原因,你可能開啟了 HeapDumpOnOutOfMemoryError 導致的️),k8s 會自動拉起一個新的。

相關文章