基礎概念
先列出幾個基礎的概念:
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區的內存是否最終穩定在一個范圍之內,如果長時間穩定在一個范圍說明溢出問題得到了解決,否則還要繼續進行分析和處理,一直到穩定為止。
參考
原文鏈接:http://www.javatang.com