Java堆內(nèi)存溢出梗概分析
任何使用過基于 Java 的企業(yè)級后端應用的軟件開發(fā)者都會遇到過這種低劣、奇怪的報錯,這些報錯來自于用戶或是測試工程師: java.lang.OutOfMemoryError:Java heap space。
為了弄清楚問題,我們必須返回到算法復雜性的計算機科學基礎,尤其是“空間”復雜性。如果我們回憶,每一個應用都有一個最壞情況特征。具體來說,在存儲維度方面,超過推薦的存儲將會被分配到應用程序上,這是不可預測但尖銳的問題。這導致了堆內(nèi)存的過度使用,因此出現(xiàn)了"內(nèi)存不夠"的情況。
這種特定情況最糟糕的部分是應用程序不能修復,并且將崩潰。任何重啟應用的嘗試 - 甚至使用***內(nèi)存(-Xmx option)- 都不是長久之計。如果不明白什么導致了堆使用的膨脹或突出,內(nèi)存使用穩(wěn)定性(即應用穩(wěn)定性)就不能保障。于是,什么才是更有效的理解關于內(nèi)存的編程問題的途徑?當內(nèi)存溢出時,明白應用程序的內(nèi)存堆和分布情況才能回答這個問題。
在這一前提下,我們將聚焦以下方面:
- 當內(nèi)存溢出時,獲取到 Java 進程中的堆轉(zhuǎn)儲。
- 明白應用程序正在遭遇的內(nèi)存問題的類型。
- 使用一個堆分析器,可以使用 Eclipse MAT 這個優(yōu)秀的開源項目來分析內(nèi)存溢出的問題。
配置應用,為堆分析做準備
任何像內(nèi)存溢出這種非確定性的、時有時無的問題對于事后的分析都是一個挑戰(zhàn)。所以,***的處理內(nèi)存溢出的方法是讓 JVM 虛擬機轉(zhuǎn)儲一份 JVM 虛擬機內(nèi)存狀態(tài)的堆文件。
Sun HotSpot JVM 有一種方法可以引導 JVM 轉(zhuǎn)儲內(nèi)存溢出時的堆狀態(tài)到一個文件中。其標準格式為 .hprof 。所以,為了實現(xiàn)這種操作,向 JVM 啟動項中添加 XX:+HeapDumpOnOutOfMemoryError 。因為內(nèi)存溢出可能經(jīng)過很長一段時間才會發(fā)生,向生產(chǎn)系統(tǒng)增加這一選項也是必須的。
如果堆轉(zhuǎn)儲 .hprof 文件必須被寫在一個特定的文件系統(tǒng)位置,那么就添加目錄途徑到 XX:HeapDumpPath 。只需確保該應用對于指定目錄途徑始終擁有寫入權(quán)限。
原因分析
101:了解內(nèi)存溢出錯誤的本質(zhì)
當嘗試去評估和了解一個內(nèi)存溢出錯誤時,***做的事情應該是觀察內(nèi)存增長特征。根據(jù)情況做出可能性的評估:
尖峰狀:這種類型的內(nèi)存溢出在某種類型的加載上會是比較激烈的。當 JVM 分配內(nèi)存給 20 個用戶時,應用程序可以正常運行。但是,如果到第 100 個用戶時可能會遭遇到內(nèi)存峰值,從而導致內(nèi)存溢出。有兩種可能的辦法去解決這個問題。
泄露:由于某些編程問題,內(nèi)存使用隨著時間的推移逐漸增加。
擁有良性垃圾回收機制的健康圖表
健康一段時間后,隨時間推移而泄露的圖表
引起內(nèi)存使用凸起、導致內(nèi)存溢出的內(nèi)存圖表
在我們了解導致使用率激增的內(nèi)存問題的本質(zhì)之后,基于從對分析中得到的推斷,下面的這些方法或許可以用來避免遭遇內(nèi)存溢出的錯誤。
解決內(nèi)存問題
修復引起內(nèi)存溢出的代碼:由于應用在某段時間內(nèi)增量添加了一個對象而沒有清除其引用(來自正在運行的應用程序的對象引用),導致不得不修復程序錯誤。例如,這一錯誤可能是插入了一個哈希表, 其中的業(yè)務對象會逐漸增加,然而業(yè)務邏輯和事務在完成之后并沒有刪除這些對象。
增加內(nèi)存***值作為一種修復方法。在了解了運行內(nèi)存特征和堆之后,可能必須增加分配的***堆內(nèi)存來避免再次發(fā)生內(nèi)存溢出,因為推薦的***內(nèi)存值不能夠滿足應用程序的穩(wěn)定性。所以,應用程序可能不得不基于堆分析器的評估,將 Java -Xmx 的 flag 信息更新成一個更高值后再來運行。
堆分析
下面我們將詳細分析如何使用一個堆分析工具來分析堆轉(zhuǎn)儲。在示例中,將使用到 Eclipse 基金會的開源工具 MAT 。
使用 MAT 進行堆分析
是時候進行深入探討了。我們將通過一系列的步驟,幫助探索在 MAT 中的不同表現(xiàn)和視圖,以獲取一個堆內(nèi)存溢出的示例并思考分析。
1. 打開內(nèi)存溢出錯誤發(fā)生時產(chǎn)生的 .hprof 堆文件。確保復制轉(zhuǎn)儲文件到一個專門的文件夾下,因為 MAT 會創(chuàng)建許多索引文件:文件 -> 打開
2. 打開轉(zhuǎn)儲文件,有內(nèi)存泄漏嫌疑報告和組件報告的選項。選擇運行泄漏嫌疑報告。
3. 泄漏嫌疑表打開后,在預覽窗口的餅狀圖會展示在每個對象基礎上保留內(nèi)存的分布情況。它顯示了內(nèi)存中的***對象(擁有***保留內(nèi)存的對象 —— 累積的內(nèi)存和引用的對象)。
4. 上面的餅圖通過聚合擁有***內(nèi)存引用(本身內(nèi)存和總內(nèi)存)的對象來展示 3 個問題嫌疑人。
讓我們逐一分情況查看,評估它是否是內(nèi)存溢出錯誤的根本原因。
可疑點 1
由 “<system class loader>” 加載的 454,570 個 “java.lang.ref.Finalizer” 實例占用了 790,205,576(47.96%)個字節(jié)。
這就是告訴我們有 454,570 個 JVM finalizer(終結(jié)器)實例占據(jù)了分配的應用內(nèi)存的近 50 %。
假設讀者知道 Java Finalizer 是做什么的,上面的信息會讓我們明白什么呢?
入門閱讀:http://stackoverflow.com/questions/2860121/why-do-finalizers-have-a-severe-performance-penalty
本質(zhì)上,開發(fā)者編寫了一些定制化的終結(jié)器去釋放一個實例的資源。這些由終結(jié)器收集的實例不在 JVM 使用單獨隊列的垃圾回收算法的范圍之內(nèi)。實際上,這種途徑比起垃圾回收機制的清理路徑更長。所以現(xiàn)在我們應該努力搞清楚這些終結(jié)器到底終結(jié)了什么?
也或許是可疑點 2 ,占據(jù)了 20% 的 sun.security.ssl.SSLSocketImpl 。我們能確認是否這些就是要被終結(jié)器終結(jié)的實例嗎?
可疑點 2
現(xiàn)在,讓我們打開在 MAT 頂部的工具按鈕下面的 Dominator 視圖。我們會看到所有的列出的類實例,經(jīng)由 MAT 解析展示出有效的堆存儲。
下一步,在 Dominator 視圖,我們嘗試理解 java.lang.Finalizer 和 sun.security.ssl.SSLSocketImpl 之間的關系。我們右鍵點擊 sun.security.ssl.SSLSocketImpl 這一列,打開 GC Roots -> exclude soft/weak references。
現(xiàn)在,MAT 將會開始繪制內(nèi)存的圖表來顯示 GC root 的路徑以及它所對應的實例引用。這會被顯示在另外一個頁面上,顯示的引用如下:
如上面引用鏈顯示,實例 SSLSocketImpl 來自于 java.lang.ref.Finalizer,整個 SSLSocketImpl 實例大約占用了 88k。我們還注意到 finalizer 鏈是一個針鏈表數(shù)據(jù)結(jié)構(gòu)它指向下一個實例。
推論:在這一點上,我們有一個明確的感覺,Java finalizer 試圖在收集 SSLSocketImpl 對象。為了解釋為什么還有很多信息沒有被收集到,我開始檢查代碼。
檢查代碼
代碼檢查需要查看是不是由 socket 套接字被關閉導致的。在這種情況下,它顯示與 I/O 相關的所有流,需要被正確地關閉。在一點上,我們懷疑 JVM 是始作俑者。實際上,在 Open JDK 6.0.XX 的 GC(垃圾收集器)上的代碼中有一個 BUG。
我希望這篇文章給你一個模式來分析 Java 應用中的錯誤是由堆存儲還是內(nèi)部問題導致的。希望你使用堆分析愉快!
擴展閱讀
Shallow heap (淺堆) vs. Retained Heap (保留堆)
淺堆是一個對象消耗的內(nèi)存。根據(jù)情況,一個對象需要 32 位或 64 位(取決于其操作系統(tǒng)架構(gòu)),對于整型為 4 字節(jié),對于 Long 型為 8 字節(jié)等等。依據(jù)堆轉(zhuǎn)儲格式,其內(nèi)存大小(比如,向 8 對齊)或許適應于更好地塑造虛擬機的真實消耗。
X 的保留集合是當 X 被垃圾回收時,那些將要被移除的對象集合。
X 的保留堆是在 X 的保留集合中所有對象的淺堆之和,也就是 X 存留的內(nèi)存。
總體講,一個對象的淺堆就是其在堆中的大小。同一個對象的保留大小就是當對象被垃圾回收時堆內(nèi)存的總量。
一些對象的主要集合,比如某一特定類的所有對象、或是由某一特定類加載器加載的所有類的所有對象、或僅僅是一些任意的對象,它們的保留集是如果那些主要集的所有對象變得不可接近時所釋放的對象集。
保留集包括這些對象和僅通過這些對象才能獲取的其它對象。保留集的大小是包含在保留集中的所有對象的堆的大小。