https://juejin.cn/post/7361234872780898316
以下來自本人拉的一個關於 Java 技術的討論群。關注公眾號:hashcon,私信進群拉你
1. 為什麼不建議開啟 HeapDumpOnOutOfMemoryError?
1.1. 開啟 HeapDumpOnOutOfMemoryError,哪些 OutOfMemoryError 會觸發 HeapDumpOnOutOfMemoryError?
開啟 HeapDumpOnOutOfMemoryError 之後,不是所有的 OutOfMemoryError 都會觸發 HeapDumpOnOutOfMemoryError,不同的 OutOfMemoryError 包括(如果對這些異常丟擲的原理詳情感興趣,請參考:zhuanlan.zhihu.com/p/265039643 ):
OutOfMemoryError: Java heap space
和OutOfMemoryError: GC overhead limit exceeded
:這兩個都是 Java 物件堆記憶體不夠了,一個是分配的時候發現剩餘空間不足,一個是到達某一界限。這兩個都會觸發HeapDumpOnOutOfMemoryError
OutOfMemoryError: unable to create native thread
:無法建立新的平臺執行緒,這個不會觸發HeapDumpOnOutOfMemoryError
OutOfMemoryError: Requested array size exceeds VM limit
:當申請的陣列大小超過堆記憶體限制,就會丟擲這個異常。這個會觸發HeapDumpOnOutOfMemoryError
OutOfMemoryError: Compressed class space
和OutOfMemoryError: Metaspace
:這兩個都和元空間相關(底層原理說明參考:juejin.cn/post/722587… ),這兩個都會觸發HeapDumpOnOutOfMemoryError
OutOfMemoryError: Cannot reserve xxx bytes of direct buffer memory (allocated: xxx, limit: xxx)
:在 DirectByteBuffer 中,首先向 Bits 類申請額度,Bits 類有一個全域性的 totalCapacity 變數,記錄著全部 DirectByteBuffer 的總大小,每次申請,都先看看是否超限,可用-XX:MaxDirectMemorySize
限制。這個不會觸發HeapDumpOnOutOfMemoryError
OutOfMemoryError: map failed
:這個是 File MMAP(檔案對映記憶體)時,如果系統記憶體不足,就會丟擲這個異常。這個不會觸發HeapDumpOnOutOfMemoryError
還有一些其他的:
- Shenandoah 分配區域點陣圖,記憶體的時候,觸發的
OutOfMemoryError
,這個會觸發HeapDumpOnOutOfMemoryError
。 OutOfMemoryError: Native heap allocation failed
,這個 Message 可能不同作業系統不一樣,但是一般都有 native heap。這個就和 Java 物件堆一般沒關係,而是其他塊記憶體無法申請導致的,這些不會觸發HeapDumpOnOutOfMemoryError
1.2. 為什麼不開啟 HeapDumpOnOutOfMemoryError
?
HeapDumpOnOutOfMemoryError
的原理:
- 進入安全點,所有應用執行緒暫停,針對 HeapDumpOnOutOfMemoryError,單執行緒(如果是 jcmd jmap 可以多執行緒)dump 堆為執行緒個數個檔案。退出安全點。
- 將上面的多個檔案,合併為一個,壓縮。
這裡的瓶頸主要在於第一步寫入,並且,主要瓶頸再磁碟 IO,我們來看下現在雲服務的磁碟 IO 標準:
- AWS EFS(普通儲存):docs.aws.amazon.com/efs/latest/…
- 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"
指定指令碼,指令碼執行:
- 微服務的下線
- 微服務的重啟
針對 spring boot,可以考慮開啟允許本地訪問 /actuator/shutdown
來關閉微服務(有群友反應丟擲 OutOfMemoryError 的時候呼叫這個會卡死,這是因為 1.2 說的原因,你可能開啟了 HeapDumpOnOutOfMemoryError 導致的️),k8s 會自動拉起一個新的。