您好,登錄后才能下訂單哦!
前言
任何使用過基于 Java 的企業級后端應用的軟件開發者都會遇到過這種低劣、奇怪的報錯,這些報錯來自于用戶或是測試工程師: java.lang.OutOfMemoryError:Java heap space。
為了弄清楚問題,我們必須返回到算法復雜性的計算機科學基礎,尤其是“空間”復雜性。如果我們回憶,每一個應用都有一個最壞情況特征。具體來說,在存儲維度方面,超過推薦的存儲將會被分配到應用程序上,這是不可預測但尖銳的問題。這導致了堆內存的過度使用,因此出現了"內存不夠"的情況。
這種特定情況最糟糕的部分是應用程序不能修復,并且將崩潰。任何重啟應用的嘗試 - 甚至使用最大內存(-Xmx option)- 都不是長久之計。如果不明白什么導致了堆使用的膨脹或突出,內存使用穩定性(即應用穩定性)就不能保障。于是,什么才是更有效的理解關于內存的編程問題的途徑?當內存溢出時,明白應用程序的內存堆和分布情況才能回答這個問題。
在這一前提下,我們將聚焦以下方面:
配置應用,為堆分析做準備
任何像內存溢出這種非確定性的、時有時無的問題對于事后的分析都是一個挑戰。所以,最好的處理內存溢出的方法是讓 JVM 虛擬機轉儲一份 JVM 虛擬機內存狀態的堆文件。
Sun HotSpot JVM 有一種方法可以引導 JVM 轉儲內存溢出時的堆狀態到一個文件中。其標準格式為 .hprof 。所以,為了實現這種操作,向 JVM 啟動項中添加 XX:+HeapDumpOnOutOfMemoryError 。因為內存溢出可能經過很長一段時間才會發生,向生產系統增加這一選項也是必須的。
如果堆轉儲 .hprof 文件必須被寫在一個特定的文件系統位置,那么就添加目錄途徑到 XX:HeapDumpPath 。只需確保該應用對于指定目錄途徑始終擁有寫入權限。
原因分析
101:了解內存溢出錯誤的本質
當嘗試去評估和了解一個內存溢出錯誤時,最先做的事情應該是觀察內存增長特征。根據情況做出可能性的評估:
擁有良性垃圾回收機制的健康圖表
健康一段時間后,隨時間推移而泄露的圖表
引起內存使用凸起、導致內存溢出的內存圖表
在我們了解導致使用率激增的內存問題的本質之后,基于從對分析中得到的推斷,下面的這些方法或許可以用來避免遭遇內存溢出的錯誤。
解決內存問題
1.修復引起內存溢出的代碼:由于應用在某段時間內增量添加了一個對象而沒有清除其引用(來自正在運行的應用程序的對象引用),導致不得不修復程序錯誤。例如,這一錯誤可能是插入了一個哈希表, 其中的業務對象會逐漸增加,然而業務邏輯和事務在完成之后并沒有刪除這些對象。
2.增加內存最大值作為一種修復方法。在了解了運行內存特征和堆之后,可能必須增加分配的最大堆內存來避免再次發生內存溢出,因為推薦的最大內存值不能夠滿足應用程序的穩定性。所以,應用程序可能不得不基于堆分析器的評估,將 Java -Xmx 的 flag 信息更新成一個更高值后再來運行。
堆分析
下面我們將詳細分析如何使用一個堆分析工具來分析堆轉儲。在示例中,將使用到 Eclipse 基金會的開源工具 MAT 。
使用 MAT 進行堆分析
是時候進行深入探討了。我們將通過一系列的步驟,幫助探索在 MAT 中的不同表現和視圖,以獲取一個堆內存溢出的示例并思考分析。
1. 打開內存溢出錯誤發生時產生的 .hprof 堆文件。確保復制轉儲文件到一個專門的文件夾下,因為 MAT 會創建許多索引文件:文件 -> 打開
2. 打開轉儲文件,有內存泄漏嫌疑報告和組件報告的選項。選擇運行泄漏嫌疑報告。
3. 泄漏嫌疑表打開后,在預覽窗口的餅狀圖會展示在每個對象基礎上保留內存的分布情況。它顯示了內存中的最大對象(擁有最高保留內存的對象 —— 累積的內存和引用的對象)。
4. 上面的餅圖通過聚合擁有最高內存引用(本身內存和總內存)的對象來展示 3 個問題嫌疑人。
讓我們逐一分情況查看,評估它是否是內存溢出錯誤的根本原因。
可疑點 1
由 “<system class loader>” 加載的 454,570 個 “java.lang.ref.Finalizer” 實例占用了 790,205,576(47.96%)個字節。
這就是告訴我們有 454,570 個 JVM finalizer(終結器)實例占據了分配的應用內存的近 50 %。
假設讀者知道 Java Finalizer 是做什么的,上面的信息會讓我們明白什么呢?
本質上,開發者編寫了一些定制化的終結器去釋放一個實例的資源。這些由終結器收集的實例不在 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 存留的內存。
總體講,一個對象的淺堆就是其在堆中的大小。同一個對象的保留大小就是當對象被垃圾回收時堆內存的總量。
一些對象的主要集合,比如某一特定類的所有對象、或是由某一特定類加載器加載的所有類的所有對象、或僅僅是一些任意的對象,它們的保留集是如果那些主要集的所有對象變得不可接近時所釋放的對象集。保留集包括這些對象和僅通過這些對象才能獲取的其它對象。保留集的大小是包含在保留集中的所有對象的堆的大小。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。