JVM故障分析及性能優化系列文章
JVM故障分析及性能優化系列之一:使用jstack定位線程堆棧信息
JVM故障分析及性能優化系列之二:jstack生成的Thread Dump日誌結構解析
JVM故障分析及性能優化系列之三:jstat命令的使用及VM Thread分析
JVM故障分析及性能優化系列之四:jstack生成的Thread Dump日誌線程狀態
JVM故障分析及性能優化系列之五:常見的Thread Dump日誌案例分析
JVM故障分析及性能優化系列之六:JVM Heap Dump(堆轉儲文件)的生成和MAT的使用
JVM故障分析及性能優化系列之七:使用MAT的Histogram和Dominator Tree定位溢出源
上一篇文章概括的介紹了JVM Heap Dump文件生成的方式以及內存分析工具MAT的概要功能,今天講解如何使用MAT的Histogram和Dominator Tree兩個視圖,定位到內存溢出源。
目錄 [隱藏]
基礎概念
先列出幾個基礎的概念:
Shallow Heap 和 Retained Heap
Shallow Heap表示對象本身佔用內存的大小,不包含對其他對象的引用,也就是對象頭加成員變量(不是成員變量的值)的總和。
Retained Heap是該對象自己的Shallow Heap,並加上從該對象能直接或間接訪問到對象的Shallow Heap之和。換句話說,Retained Heap是該對象GC之後所能回收到內存的總和。
把內存中的對象看成下圖中的節點,並且對象和對象之間互相引用。這裏有一個特殊的節點GC Roots,這就是reference chain的起點。
從obj1入手,上圖中藍色節點代表僅僅只有通過obj1才能直接或間接訪問的對象。因爲可以通過GC Roots訪問,所以左圖的obj3不是藍色節點;而在右圖卻是藍色,因爲它已經被包含在retained集合內。所以對於左圖,obj1的retained size是obj1、obj2、obj4的shallow size總和;右圖的retained size是obj1、obj2、obj3、obj4的shallow size總和。obj2的retained size可以通過相同的方式計算。
對象引用(Reference)
對象引用按從最強到最弱有如下級別,不同的引用(可到達性)級別反映了對象的生命週期:
- 強引用(Strong Ref):通常我們編寫的代碼都是強引用,於此相對應的是強可達性,只有去掉強可達性,對象才能被回收。
- 軟引用(Soft Ref):對應軟可達性,只要有足夠的內存就一直保持對象,直到發現內存不足且沒有強引用的時候纔回收對象。
- 弱引用(Weak Ref):比軟引用更弱,當發現不存在強引用的時候會立即回收此類型的對象,而不需要等到內存不足。通過java.lang.ref.WeakReference和java.util.WeakHashMap類實現。
- 虛引用(Phantom Ref):根本不會在內存中保持該類型的對象,只能使用虛引用本身,一般用於在進入finalize()方法後進行特殊的清理過程,通過java.lang.ref.PhantomReference實現。
GC Roots和Reference Chain
JVM在進行GC的時候是通過使用可達性來判斷對象是否存活,通過GC Roots(GC根節點)的對象作爲起始點,從這些節點開始進行向下搜索,搜索所走過的路徑成爲Reference Chain(引用鏈),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。
如下圖所示,對象object 5、object 6、object 7雖然互相有關聯,它們的引用並不爲0,但是它們到GC Roots是不可達的,因此它們將會被判定爲是可回收的對象。
Histogram(直方圖)視圖
點擊工具欄上的 圖標可以打開Histogram(直方圖)視圖,可以列出每個類產生的實例數量,以及所佔用的內存大小和百分比。主界面如下圖所示:
圖中Shallow Heap 和 Retained Heap分別表示對象自身不包含引用的大小和對象自身幷包含引用的大小,具體請參考下面 Shallow Heap 和 Retained Heap 部分的內容。默認的大小單位是 Bytes,可以在 Window - Preferences 菜單中設置單位,圖中設置的是KB。
通過直方圖視圖可以很容易找到佔用內存最多的幾個類(通過Retained Heap排序),還可以通過其他方式進行分組(見下圖)。
如果存在內存溢出,時間久了溢出類的實例數量或者內存佔比會越來越多,排名也越來越靠前。可以點擊工具類上的 圖標進行對比,通過多次對比不同時間點下的直方圖對比就很容易把溢出的類找出來。
還有一種對比直方圖的方式,首先通過 Window 菜單打開 Navigation History 視圖,選中直方圖右鍵並選中 Add to Compare Basket項目,將直方圖添加到 Compare Basket 中。
然後在 Compare Basket 中點擊右上角的 按鈕,可以分別列出對比的所有結果,見下圖:
並且在上面的可以設置不同的對比方式。
Dominator Tree視圖
點擊工具欄上的 圖標可以打開Dominator Tree(支配樹)視圖,在此視圖中列出了每個對象(Object Instance)與其引用關係的樹狀結構,同時包含了佔用內存的大小和百分比。
通過Dominator Tree視圖可以很容易的找出佔用內存最多的幾個對象(根據Retained Heap或Percentage排序),和Histogram類似,可以通過不同的方式進行分組顯示:
定位溢出源
Histogram視圖和Dominator Tree視圖的角度不同,前者是基於類的角度,後者是基於對象實例的角度,並且可以更方便的看出其引用關係。
首先,在兩個視圖中找出疑似溢出的對象或者類(可以通過Retained Heap排序,並且可以在Class Name中輸入正則表達式的關鍵詞只顯示指定的類名),然後右鍵選擇Path To GC Roots(Histogram中沒有此項)或Merge Shortest Paths to GC Roots,然後選擇 exclude all phantom/weak/soft etc. reference:
GC Roots意爲GC根節點,其含義見上面的 GC Roots和Reference Chain 部分,後面的 exclude all phantom/weak/soft etc. reference 意思是排除虛引用、弱引用和軟引用,即只剩下強引用,因爲除了強引用之外,其他的引用都可以被JVM GC掉,如果一個對象始終無法被GC,就說明有強引用存在,從而導致在GC的過程中一直得不到回收,最終就內存溢出了。
通過結果就可以很方便的定位到具體的代碼,然後分析是什麼原因無法釋放該對象,比如被緩存了或者沒有使用單例模式等等。
下面是執行的結果:
上圖中保留了大量的VelocitySqlBulder的外部引用,後來查看了代碼,原來每次調用的時候都實例化一個新的對象,由於VelocitySqlBulder類是無狀態的工具類,因此修改爲單例方式就可以解決這個問題。
後續觀察
根據上面分析的結果對問題進行處理之後,再對照之前的操作,看看對象是否還再持續增長,如果沒有就說明這個地方的問題已經解決了。
最後再用 jstat 持續跟蹤一段時間,看看Old和Perm區的內存是否最終穩定在一個範圍之內,如果長時間穩定在一個範圍說明溢出問題得到了解決,否則還要繼續進行分析和處理,一直到穩定爲止。
參考資料:
Memory Analyzer Tool 使用手記
使用Eclipse Memory Analyzer分析內存
使用Memory Analyzer tool(MAT)分析內存泄漏(一)
Shallow and retained sizes
JVM內存回收理論與實現
GC roots
一次使用Eclipse Memory Analyzer分析Tomcat內存溢出