來自:開源中國 協作翻譯
原文:Java Out of Memory Heap Analysis
連結:https://dzone.com/articles/java-out-of-memory-heap-analysis
譯者:dreamanzhao, 無若
任何使用過基於 Java 的企業級後端應用的軟體開發者都會遇到過這種低劣、奇怪的報錯,這些報錯來自於使用者或是測試工程師: java.lang.OutOfMemoryError:Java heap space。
為了弄清楚問題,我們必須傳回到演演算法複雜性的電腦科學基礎,尤其是“空間”複雜性。如果我們回憶,每一個應用都有一個最壞情況特徵。具體來說,在儲存維度方面,超過推薦的儲存將會被分配到應用程式上,這是不可預測但尖銳的問題。這導致了堆記憶體的過度使用,因此出現了”記憶體不夠”的情況。
這種特定情況最糟糕的部分是應用程式不能修複,並且將崩潰。任何重啟應用的嘗試 – 甚至使用最大記憶體(-Xmx option)- 都不是長久之計。如果不明白什麼導致了堆使用的膨脹或突出,記憶體使用穩定性(即應用穩定性)就不能保障。於是,什麼才是更有效的理解關於記憶體的程式設計問題的途徑?當記憶體上限溢位時,明白應用程式的記憶體堆和分佈情況才能回答這個問題。
在這一前提下,我們將聚焦以下方面:
-
當記憶體上限溢位時,獲取到 Java 行程中的堆轉儲。
-
明白應用程式正在遭遇的記憶體問題的型別。
-
使用一個堆分析器,可以使用 Eclipse MAT 這個優秀的開源專案來分析記憶體上限溢位的問題。
配置應用,為堆分析做準備
任何像記憶體上限溢位這種非確定性的、時有時無的問題對於事後的分析都是一個挑戰。所以,最好的處理記憶體上限溢位的方法是讓 JVM 虛擬機器轉儲一份 JVM 虛擬機器記憶體狀態的堆檔案。
Sun HotSpot JVM 有一種方法可以引導 JVM 轉儲記憶體上限溢位時的堆狀態到一個檔案中。其標準格式為 .hprof 。所以,為了實現這種操作,向 JVM 啟動項中新增 XX:+HeapDumpOnOutOfMemoryError 。因為記憶體上限溢位可能經過很長一段時間才會發生,向生產系統增加這一選項也是必須的。
如果堆轉儲 .hprof 檔案必須被寫在一個特定的檔案系統位置,那麼就新增目錄途徑到 XX:HeapDumpPath 。只需確保該應用對於指定目錄途徑始終擁有寫入許可權。
原因分析
101:瞭解記憶體上限溢位錯誤的本質
當嘗試去評估和瞭解一個記憶體上限溢位錯誤時,最先做的事情應該是觀察記憶體增長特徵。根據情況做出可能性的評估:
-
尖峰狀:這種型別的記憶體上限溢位在某種型別的載入上會是比較激烈的。當 JVM 分配記憶體給 20 個使用者時,應用程式可以正常執行。但是,如果到第 100 個使用者時可能會遭遇到記憶體峰值,從而導致記憶體上限溢位。有兩種可能的辦法去解決這個問題。
-
洩露:由於某些程式設計問題,記憶體使用隨著時間的推移逐漸增加。
擁有良性垃圾回收機制的健康圖表
健康一段時間後,隨時間推移而洩露的圖表
引起記憶體使用凸起、導致記憶體上限溢位的記憶體圖表
在我們瞭解導致使用率激增的記憶體問題的本質之後,基於從對分析中得到的推斷,下麵的這些方法或許可以用來避免遭遇記憶體上限溢位的錯誤。
解決記憶體問題
-
修複引起記憶體上限溢位的程式碼:由於應用在某段時間內增量添加了一個物件而沒有清除其取用(來自正在執行的應用程式的物件取用),導致不得不修複程式錯誤。例如,這一錯誤可能是插入了一個雜湊表, 其中的業務物件會逐漸增加,然而業務邏輯和事務在完成之後並沒有刪除這些物件。
-
增加記憶體最大值作為一種修複方法。在瞭解了執行記憶體特徵和堆之後,可能必須增加分配的最大堆記憶體來避免再次發生記憶體上限溢位,因為推薦的最大記憶體值不能夠滿足應用程式的穩定性。所以,應用程式可能不得不基於堆分析器的評估,將 Java -Xmx 的 flag 資訊更新成一個更高值後再來執行。
堆分析
下麵我們將詳細分析如何使用一個堆分析工具來分析堆轉儲。在示例中,將使用到 Eclipse 基金會的開源工具 MAT 。
使用 MAT 進行堆分析
是時候進行深入探討了。我們將透過一系列的步驟,幫助探索在 MAT 中的不同表現和檢視,以獲取一個堆記憶體上限溢位的示例並思考分析。
1. 開啟記憶體上限溢位錯誤發生時產生的 .hprof 堆檔案。確保複製轉儲檔案到一個專門的檔案夾下,因為 MAT 會建立許多索引檔案:檔案 -> 開啟
2. 開啟轉儲檔案,有記憶體洩漏嫌疑報告和元件報告的選項。選擇執行洩漏嫌疑報告。
3. 洩漏嫌疑表開啟後,在預覽視窗的餅狀圖會展示在每個物件基礎上保留記憶體的分佈情況。它顯示了記憶體中的最大物件(擁有最高保留記憶體的物件 —— 累積的記憶體和取用的物件)。
4. 上面的餅圖透過聚合擁有最高記憶體取用(本身記憶體和總記憶體)的物件來展示 3 個問題嫌疑人。
讓我們逐一分情況檢視,評估它是否是記憶體上限溢位錯誤的根本原因。
可疑點 1
由 “
這就是告訴我們有 454,570 個 JVM finalizer(終結器)實體佔據了分配的應用記憶體的近 50 %。
假設讀者知道 Java Finalizer 是做什麼的,上面的資訊會讓我們明白什麼呢?
入門閱讀:http://stackoverflow.com/questions/2860121/why-do-finalizers-have-a-severe-performance-penalty
本質上,開發者編寫了一些定製化的終結器去釋放一個實體的資源。這些由終結器收集的實體不在 JVM 使用單獨佇列的垃圾回收演演算法的範圍之內。實際上,這種途徑比起垃圾回收機制的清理路徑更長。所以現在我們應該努力搞清楚這些終結器到底終結了什麼?
也或許是可疑點 2 ,佔據了 20% 的 sun.security.ssl.SSLSocketImpl 。我們能確認是否這些就是要被終結器終結的實體嗎?
可疑點 2
現在,讓我們開啟在 MAT 頂部的工具按鈕下麵的 Dominator 檢視。我們會看到所有的列出的類實體,經由 MAT 解析展示出有效的堆儲存。
下一步,在 Dominator 檢視,我們嘗試理解 java.lang.Finalizer 和 sun.security.ssl.SSLSocketImpl 之間的關係。我們右鍵點擊 sun.security.ssl.SSLSocketImpl 這一列,開啟 GC Roots -> exclude soft/weak references。
現在,MAT 將會開始繪製記憶體的圖表來顯示 GC root 的路徑以及它所對應的實體取用。這會被顯示在另外一個頁面上,顯示的取用如下:
如上面取用鏈顯示,實體 SSLSocketImpl 來自於 java.lang.ref.Finalizer,整個 SSLSocketImpl 實體大約佔用了 88k。我們還註意到 finalizer 鏈是一個針連結串列資料結構它指向下一個實體。
推論:在這一點上,我們有一個明確的感覺,Java finalizer 試圖在收集 SSLSocketImpl 物件。為瞭解釋為什麼還有很多資訊沒有被收集到,我開始檢查程式碼。
檢查程式碼
程式碼檢查需要檢視是不是由 socket 套接字被關閉導致的。在這種情況下,它顯示與 I/O 相關的所有流,需要被正確地關閉。在一點上,我們懷疑 JVM 是始作俑者。實際上,在 Open JDK 6.0.XX 的 GC(垃圾收集器)上的程式碼中有一個 BUG。
我希望這篇文章給你一個樣式來分析 Java 應用中的錯誤是由堆儲存還是內部問題導致的。希望你使用堆分析愉快!
擴充套件閱讀
Shallow heap (淺堆) vs. Retained Heap (保留堆)
淺堆是一個物件消耗的記憶體。根據情況,一個物件需要 32 位或 64 位(取決於其作業系統架構),對於整型為 4 位元組,對於 Long 型為 8 位元組等等。依據堆轉儲格式,其記憶體大小(比如,向 8 對齊)或許適應於更好地塑造虛擬機器的真實消耗。
X 的保留集合是當 X 被垃圾回收時,那些將要被移除的物件集合。
X 的保留堆是在 X 的保留集合中所有物件的淺堆之和,也就是 X 存留的記憶體。
總體講,一個物件的淺堆就是其在堆中的大小。同一個物件的保留大小就是當物件被垃圾回收時堆記憶體的總量。
一些物件的主要集合,比如某一特定類的所有物件、或是由某一特定類載入器載入的所有類的所有物件、或僅僅是一些任意的物件,它們的保留集是如果那些主要集的所有物件變得不可接近時所釋放的物件集。
保留集包括這些物件和僅透過這些物件才能獲取的其它物件。保留集的大小是包含在保留集中的所有物件的堆的大小。
●編號650,輸入編號直達本文
●輸入m獲取文章目錄
大資料與人工智慧
更多推薦《18個技術類公眾微信》
涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。