使用Memory Analyzer tool(MAT)分析內存泄漏

前言的前言

本文是自2005年8月以來,首次在一個月之內發佈三篇文章。謹以此文獻給這麼多年始終不濟的我。所謂少不入川,而今已非年少。北漂快兩年了,何時能回到故鄉,回去後又會怎樣,也許永遠是個未知……

 

前言 

在平時工作過程中,有時會遇到OutOfMemoryError,我們知道遇到Error一般表明程序存在着嚴重問題,可能是災難性的。所以找出是什麼原因造成OutOfMemoryError非常重要。現在向大家引薦Eclipse Memory Analyzer tool(MAT),來化解我們遇到的難題。如未說明,本文均使用Java 5.0 on Windows XP SP3環境。

 

爲什麼用MAT

之前的觀點,我認爲使用實時profiling/monitoring之類的工具,用一種非常實時的方式來分析哪裏存在內存泄漏是很正確的。年初使用了某profiler工具測試消息中間件中存在的內存泄漏,發現在吞吐量很高的時候profiler工具自己也無法響應,這讓人很頭痛。後來瞭解到這樣的工具本身就要消耗性能,且在某些條件下還發現不了泄漏。所以,分析離線數據就非常重要了,MAT正是這樣一款工具。

 

爲何會內存溢出 

我們知道JVM根據generation(代)來進行GC,根據下圖所示,一共被分爲young generation(年輕代)、tenured generation(老年代)、permanent generation(永久代, perm gen),perm gen(或稱Non-Heap 非堆)是個異類,稍後會講到。注意,heap空間不包括perm gen。


絕大多數的對象都在young generation被分配,也在young generation被收回,當young generation的空間被填滿,GC會進行minor collection(次回收),這次回收不涉及到heap中的其他generation,minor collection根據weak generational hypothesis(弱年代假設)來假設young generation中大量的對象都是垃圾需要回收,minor collection的過程會非常快。young generation中未被回收的對象被轉移到tenured generation,然而tenured generation也會被填滿,最終觸發major collection(主回收),這次回收針對整個heap,由於涉及到大量對象,所以比minor collection慢得多。

 

JVM有三種垃圾回收器,分別是throughput collector,用來做並行young generation回收,由參數-XX:+UseParallelGC啓動;concurrent low pause collector,用來做tenured generation併發回收,由參數-XX:+UseConcMarkSweepGC啓動;incremental low pause collector,可以認爲是默認的垃圾回收器。不建議直接使用某種垃圾回收器,最好讓JVM自己決斷,除非自己有足夠的把握。

 

Heap中各generation空間是如何劃分的?通過JVM的-Xmx=n參數可指定最大heap空間,而-Xms=n則是指定最小heap空間。在JVM初始化的時候,如果最小heap空間小於最大heap空間的話,如上圖所示JVM會把未用到的空間標註爲Virtual。除了這兩個參數還有-XX:MinHeapFreeRatio=n和 -XX:MaxHeapFreeRatio=n來分別控制最大、最小的剩餘空間與活動對象之比例。在32位Solaris SPARC操作系統下,默認值如下,在32位windows xp下,默認值也差不多。


參數

默認值

MinHeapFreeRatio

40

MaxHeapFreeRatio

70

-Xms

3670k

-Xmx

64m


由於tenured generation的major collection較慢,所以tenured generation空間小於young generation的話,會造成頻繁的major collection,影響效率。Server JVM默認的young generation和tenured generation空間比例爲1:2,也就是說young generation的eden和survivor空間之和是整個heap(當然不包括perm gen)的三分之一,該比例可以通過-XX:NewRatio=n參數來控制,而Client JVM默認的-XX:NewRatio是8。至於調整young generation空間大小的NewSize=n和MaxNewSize=n參數就不講了,請參考後面的資料。

 

young generation中倖存的對象被轉移到tenured generation,但不幸的是concurrent collector線程在這裏進行major collection,而在回收任務結束前空間被耗盡了,這時將會發生Full Collections(Full GC),整個應用程序都會停止下來直到回收完成。Full GC是高負載生產環境的噩夢……

 

現在來說說異類perm gen,它是JVM用來存儲無法在Java語言級描述的對象,這些對象分別是類和方法數據(與class loader有關)以及interned strings(字符串駐留)。一般32位OS下perm gen默認64m,可通過參數-XX:MaxPermSize=n指定,JVM Memory Structure一文說,對於這塊區域,沒有更詳細的文獻了,神祕。

 

回到問題“爲何會內存溢出?”。

要回答這個問題又要引出另外一個話題,既什麼樣的對象GC纔會回收?當然是GC發現通過任何reference chain(引用鏈)無法訪問某個對象的時候,該對象即被回收。名詞GC Roots正是分析這一過程的起點,例如JVM自己確保了對象的可到達性(那麼JVM就是GC Roots),所以GC Roots就是這樣在內存中保持對象可到達性的,一旦不可到達,即被回收。通常GC Roots是一個在current thread(當前線程)的call stack(調用棧)上的對象(例如方法參數和局部變量),或者是線程自身或者是system class loader(系統類加載器)加載的類以及native code(本地代碼)保留的活動對象。所以GC Roots是分析對象爲何還存活於內存中的利器。知道了什麼樣的對象GC纔會回收後,再來學習下對象引用都包含哪些吧。

 

從最強到最弱,不同的引用(可到達性)級別反映了對象的生命週期。

l  Strong Ref(強引用):通常我們編寫的代碼都是Strong Ref,於此對應的是強可達性,只有去掉強可達,對象才被回收。

l  Soft Ref(軟引用):對應軟可達性,只要有足夠的內存,就一直保持對象,直到發現內存吃緊且沒有Strong Ref時纔回收對象。一般可用來實現緩存,通過java.lang.ref.SoftReference類實現。

l  Weak Ref(弱引用):比Soft Ref更弱,當發現不存在Strong Ref時,立刻回收對象而不必等到內存吃緊的時候。通過java.lang.ref.WeakReference和java.util.WeakHashMap類實現。

l  Phantom Ref(虛引用):根本不會在內存中保持任何對象,你只能使用Phantom Ref本身。一般用於在進入finalize()方法後進行特殊的清理過程,通過 java.lang.ref.PhantomReference實現。

 

有了上面的種種我相信很容易就能把heap和perm gen撐破了吧,是的利用Strong Ref,存儲大量數據,直到heap撐破;利用interned strings(或者class loader加載大量的類)把perm gen撐破。

 

關於shallow size、retained size 

Shallow size就是對象本身佔用內存的大小,不包含對其他對象的引用,也就是對象頭加成員變量(不是成員變量的值)的總和。在32位系統上,對象頭佔用8字節,int佔用4字節,不管成員變量(對象或數組)是否引用了其他對象(實例)或者賦值爲null它始終佔用4字節。故此,對於String對象實例來說,它有三個int成員(3*4=12字節)、一個char[]成員(1*4=4字節)以及一個對象頭(8字節),總共3*4 +1*4+8=24字節。根據這一原則,對String a=”rosen jiang”來說,實例a的shallow size也是24字節(很多人對此有爭議,請看官甄別並留言給我)。

 

Retained size是該對象自己的shallow size,加上從該對象能直接或間接訪問到對象的shallow size之和。換句話說,retained size是該對象被GC之後所能回收到內存的總和。爲了更好的理解retained size,不妨看個例子。

 

把內存中的對象看成下圖中的節點,並且對象和對象之間互相引用。這裏有一個特殊的節點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可以通過相同的方式計算。

 

Heap Dump 

heap dump是特定時間點,java進程的內存快照。有不同的格式來存儲這些數據,總的來說包含了快照被觸發時java對象和類在heap中的情況。由於快照只是一瞬間的事情,所以heap dump中無法包含一個對象在何時、何地(哪個方法中)被分配這樣的信息。

 

在不同平臺和不同java版本有不同的方式獲取heap dump,而MAT需要的是HPROF格式的heap dump二進制文件。想無需人工干預的話,要這樣配置JVM參數:-XX:-HeapDumpOnOutOfMemoryError,當錯誤發生時,會自動生成heap dump,在生產環境中,只有用這種方式。如果你想自己控制什麼時候生成heap dump,在Windows+JDK6環境中可利用JConsole工具,而在Linux或者Mac OS X環境下均可使用JDK5、6自帶的jmap工具。當然,還可以配置JVM參數:-XX:+HeapDumpOnCtrlBreak,也就是在控制檯使用Ctrl+Break鍵來生成heap dump。由於我是windows+JDK5,所以選擇了-XX:-HeapDumpOnOutOfMemoryError這種方式,更多配置請參考MAT Wiki

 

參考資料 

MAT Wiki

Interned Strings

Strong,Soft,Weak,Phantom Reference

Tuning Garbage Collection with the 5.0 Java[tm] Virtual Machine

Permanent Generation

Understanding Weak References譯文

Java HotSpot VM Options

Shallow and retained sizes

JVM Memory Structure

GC roots


----------------------------------------------------------------------------------------------------------------------

前言的前言
寫blog就是好,在大前提下可以想說什麼寫什麼,不像投稿那麼字字斟酌。上週末回了趟成都辦事,所以本文來遲了。K117從達州經由達成線往成都方向走的時候,發現鐵路邊有條河,儘管我現在也不知道其名字,但已被其深深的陶醉。河很寬且水流平緩,河邊山丘森林密佈,民房星星點點的分佈在河邊,河裏偶爾些小船。當時我就在想,在這裏生活是多麼的愜意,夏天還可以下去暢遊一番,閒來無事也可垂釣。唉,越來越討厭北漂了。

前言
使用Memory Analyzer tool(MAT)分析內存泄漏(一)中,我介紹了內存泄漏的前因後果。在本文中,將介紹MAT如何根據heap dump分析泄漏根源。由於測試範例可能過於簡單,很容易找出問題,但我期待藉此舉一反三。
一開始不得不說說ClassLoader,本質上,它的工作就是把磁盤上的類文件讀入內存,然後調用 java.lang.ClassLoader.defineClass方法告訴系統把內存鏡像處理成合法的字節碼。Java提供了抽象類 ClassLoader,所有用戶自定義類裝載器都實例化自ClassLoader的子類。system class loader在沒有指定裝載器的情況下默認裝載用戶類,在Sun Java 1.5中既sun.misc.Launcher$AppClassLoader。更詳細的內容請參看下面的資料。


準備heap dump
請看下面的Pilot類,沒啥特殊的。

/**
 * Pilot class
 * 
@author rosen jiang
 
*/

package org.rosenjiang.bo;

public class
 Pilot{
    
    String name;
    
int
 age;
    
    
public Pilot(String a, int
 b){
        name 
=
 a;
        age 
=
 b;
    }
}


然後再看OOMHeapTest類,它是如何撐破heap dump的。

/**
 * OOMHeapTest class
 * 
@author rosen jiang
 
*/

package org.rosenjiang.test;

import
 java.util.Date;
import
 java.util.HashMap;
import
 java.util.Map;
import
 org.rosenjiang.bo.Pilot;

public class
 OOMHeapTest {
    
public static void
 main(String[] args){
        oom();
    }
    
    
private static void
 oom(){
        Map
<String, Pilot> map = new HashMap<String, Pilot>
();
        Object[] array 
= new Object[1000000
];
        
for(int i=0; i<1000000; i++
){
            String d 
= new
 Date().toString();
            Pilot p 
= new
 Pilot(d, i);
            map.put(i
+"rosen jiang"
, p);
            array[i]
=
p;
        }
    }
}


是的,上面構造了很多的Pilot類實例,向數組和map中放。由於是Strong Ref,GC自然不會回收這些對象,一直放在heap中直到溢出。當然在運行前,先要在Eclipse中配置VM參數 -XX:+HeapDumpOnOutOfMemoryError。好了,一會兒功夫內存溢出,控制檯打出如下信息。

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3600.hprof 
Heap dump file created 
[78233961 bytes in 1.995 secs]

Exception in thread 
"main" java.lang.OutOfMemoryError: Java heap space



java_pid3600.hprof既是heap dump,可以在OOMHeapTest類所在的工程根目錄下找到。

MAT安裝
話分兩頭說,有了heap dump還得安裝MAT。可以在http://www.eclipse.org/mat/downloads.php選擇合適的方式安裝。安裝完成後切換到Memory Analyzer視圖。在Eclipse的左上角有Open Heap Dump按鈕,按照剛纔說的路徑找到java_pid3600.hprof文件並打開。解析hprof文件會花些時間,然後會彈出嚮導,直接Finish 即可。稍後會看到下圖所示的界面。



MAT工具分析了heap dump後在界面上非常直觀的展示了一個餅圖,該圖深色區域被懷疑有內存泄漏,可以發現整個heap才64M內存,深色區域就佔了99.5%。接下來是一個簡短的描述,告訴我們main線程佔用了大量內存,並且明確指出system class loader加載的"java.lang.Thread"實例有內存聚集,並建議用關鍵字"java.lang.Thread"進行檢查。所以,MAT通過簡單的兩句話就說明了問題所在,就算使用者沒什麼處理內存問題的經驗。在下面還有一個"Details"鏈接,在點開之前不妨考慮一個問題:爲何對象實例會聚集在內存中,爲何存活(而未被GC)?是的——Strong Ref,那麼再走近一些吧。



點擊了"Details"鏈接之後,除了在上一頁看到的描述外,還有Shortest Paths To the Accumulation Point和Accumulated Objects部分,這裏說明了從GC root到聚集點的最短路徑,以及完整的reference chain。觀察Accumulated Objects部分,java.util.HashMap和java.lang.Object[1000000]實例的retained heap(size)最大,在上一篇文章中我們知道retained heap代表從該類實例沿着reference chain往下所能收集到的其他類實例的shallow heap(size)總和,所以明顯類實例都聚集在HashMap和Object數組中了。這裏我們發現一個有趣的現象,既Object數組的 shallow heap和retained heap竟然一樣,通過
Shallow and retained sizes一文可知,數組的shallow heap和一般對象(非數組)不同,依賴於數組的長度和裏面的元素的類型,對數組求shallow heap,也就是求數組集合內所有對象的shallow heap之和。好,再來看org.rosenjiang.bo.Pilot對象實例的shallow heap爲何是16,因爲對象頭是8字節,成員變量int是4字節、String引用是4字節,故總共16字節。



接着往下看,來到了Accumulated Objects by Class區域,顧名思義,這裏能找到被聚集的對象實例的類名。org.rosenjiang.bo.Pilot類上頭條了,被實例化了290,325 次,再返回去看程序,我承認是故意這麼幹的。還有很多有用的報告可用來協助分析問題,只是本文中的例子太簡單,也用不上。以後如有用到,一定撰文詳細敘述。

又是perm gen
我們在上一篇文章中知道,perm gen是個異類,裏面存儲了類和方法數據(與class loader有關)以及interned strings(字符串駐留)。在heap dump中沒有包含太多的perm gen信息。那麼我們就用這些少量的信息來解決問題吧。

看下面的代碼,利用interned strings把perm gen撐破了。

/**
 * OOMPermTest class
 * 
@author rosen jiang
 
*/

package org.rosenjiang.test;

public class
 OOMPermTest {
    
public static void
 main(String[] args){
        oom();
    }
    
    
private static void
 oom(){
        Object[] array 
= new Object[10000000
];
        
for(int i=0; i<10000000; i++
){
            String d 
=
 String.valueOf(i).intern();
            array[i]
=
d;
        }
    }
}


控制檯打印如下的信息,然後把java_pid1824.hprof文件導入到MAT。其實在MAT裏,看到的狀況應該和 “OutOfMemoryError: Java heap space”差不多(用了數組),因爲heap dump並沒有包含interned strings方面的任何信息。只是在這裏需要強調,使用intern()方法的時候應該多加註意。

java.lang.OutOfMemoryError: PermGen space
Dumping heap to java_pid1824.hprof 
Heap dump file created 
[121273334 bytes in 2.845 secs]

Exception in thread 
"main" java.lang.OutOfMemoryError: PermGen space



倒是在思考如何把class loader撐破廢了些心思。經過嘗試,發現使用ASM來動態生成類才能達到目的。ASM(http://asm.objectweb.org)的主要作用是處理已編譯類(compiled class),能對已編譯類進行生成、轉換、分析(功能之一是實現動態代理),而且它運行起來足夠的快和小巧,文檔也全面,實屬居家必備之良品。ASM提供了core API和tree API,前者是基於事件的方式,後者是基於對象的方式,類似於XML的SAX、DOM解析,但是使用tree API性能會有損失。既然下面要用到ASM,這裏不得不囉嗦下已編譯類的結構,包括:
    1、修飾符(例如public、private)、類名、父類名、接口和annotation部分。
    2、類成員變量聲明,包括每個成員的修飾符、名字、類型和annotation。
    3、方法和構造函數描述,包括修飾符、名字、返回和傳入參數類型,以及annotation。當然還包括這些方法或構造函數的具體Java字節碼。
    4、常量池(constant pool)部分,constant pool是一個包含類中出現的數字、字符串、類型常量的數組。



已編譯類和原來的類源碼區別在於,已編譯類只包含類本身,內部類不會在已編譯類中出現,而是生成另外一個已編譯類文件;其二,已編譯類中沒有註釋;其三,已編譯類沒有package和import部分。
這裏還得說說已編譯類對Java類型的描述,對於原始類型由單個大寫字母表示,Z代表boolean、C代表char、B代表byte、S代表 short、I代表int、F代表float、J代表long、D代表double;而對類類型的描述使用內部名(internal name)外加前綴L和後面的分號共同表示來表示,所謂內部名就是帶全包路徑的表示法,例如String的內部名是java/lang/String;對於數組類型,使用單方括號加上數據元素類型的方式描述。最後對於方法的描述,用圓括號來表示,如果返回是void用V表示,具體參考下圖。



下面的代碼中會使用ASM core API,注意接口ClassVisitor是核心,FieldVisitor、MethodVisitor都是輔助接口。ClassVisitor應該按照這樣的方式來調用:visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*( visitInnerClass | visitField | visitMethod )* visitEnd。就是說visit方法必須首先調用,再調用最多一次的visitSource,再調用最多一次的visitOuterClass方法,接下來再多次調用visitAnnotation和visitAttribute方法,最後是多次調用visitInnerClass、 visitField和visitMethod方法。調用完後再調用visitEnd方法作爲結尾。

注意ClassWriter類,該類實現了ClassVisitor接口,通過toByteArray方法可以把已編譯類直接構建成二進制形式。由於我們要動態生成子類,所以這裏只對ClassWriter感興趣。首先是抽象類原型:

/**
 * 
@author rosen jiang
 * MyAbsClass class
 
*/

package org.rosenjiang.test;

public abstract class
 MyAbsClass {
    
int LESS = -1
;
    
int EQUAL = 0
;
    
int GREATER = 1
;
    
abstract int
 absTo(Object o);
}


其次是自定義類加載器,實在沒法,ClassLoader的defineClass方法都是protected的,要加載字節數組形式(因爲toByteArray了)的類只有繼承一下自己再實現。

/**
 * 
@author rosen jiang
 * MyClassLoader class
 
*/

package org.rosenjiang.test;

public class MyClassLoader extends
 ClassLoader {
    
public Class defineClass(String name, byte
[] b) {
        
return defineClass(name, b, 0
, b.length);
    }
}


最後是測試類。

/**
 * 
@author rosen jiang
 * OOMPermTest class
 
*/

package org.rosenjiang.test;

import
 java.util.ArrayList;
import
 java.util.List;
import
 org.objectweb.asm.ClassWriter;
import
 org.objectweb.asm.Opcodes;

public class
 OOMPermTest {
    
public static void main(String[] args)
{
        OOMPermTest o 
= new
 OOMPermTest();
        o.oom();
    }

    
private void oom()
{
        
try
 {
            ClassWriter cw 
= new ClassWriter(0
);
            cw.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC 
+
 Opcodes.ACC_ABSTRACT,
            
"org/rosenjiang/test/MyAbsClass"null"java/lang/Object"
,
            
new
 String[] {});
            cw.visitField(Opcodes.ACC_PUBLIC 
+ Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "LESS""I"
,
            
nullnew Integer(-1
)).visitEnd();
            cw.visitField(Opcodes.ACC_PUBLIC 
+ Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "EQUAL""I"
,
            
nullnew Integer(0
)).visitEnd();
            cw.visitField(Opcodes.ACC_PUBLIC 
+ Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "GREATER""I"
,
            
nullnew Integer(1
)).visitEnd();
            cw.visitMethod(Opcodes.ACC_PUBLIC 
+ Opcodes.ACC_ABSTRACT, "absTo"
,
            
"(Ljava/lang/Object;)I"nullnull
).visitEnd();
            cw.visitEnd();
            
byte[] b =
 cw.toByteArray();

            List
<ClassLoader> classLoaders = new ArrayList<ClassLoader>
();
            
while (true
) {
                MyClassLoader classLoader 
= new
 MyClassLoader();
                classLoader.defineClass(
"org.rosenjiang.test.MyAbsClass"
, b);
                classLoaders.add(classLoader);
            }
        } 
catch
 (Exception e) {
            e.printStackTrace();
        }
    }
}


不一會兒,控制檯就報錯了。

java.lang.OutOfMemoryError: PermGen space
Dumping heap to java_pid3023.hprof 
Heap dump file created 
[92593641 bytes in 2.405 secs]

Exception in thread 
"main" java.lang.OutOfMemoryError: PermGen space


打開java_pid3023.hprof文件,注意看下圖的Classes: 88.1k和Class Loader: 87.7k部分,從這點可看出class loader加載了大量的類。



更進一步分析,點擊上圖中紅框線圈起來的按鈕,選擇Java Basics——Class Loader Explorer功能。打開後能看到下圖所示的界面,第一列是class loader名字;第二列是class loader已定義類(defined classes)的個數,這裏要說一下已定義類和已加載類(loaded classes)了,當需要加載類的時候,相應的class loader會首先把請求委派給父class loader,只有當父class loader加載失敗後,該class loader纔會自己定義並加載類,這就是Java自己的“雙親委派加載鏈”結構;第三列是class loader所加載的類的實例數目。



在Class Loader Explorer這裏,能發現class loader是否加載了過多的類。另外,還有Duplicate Classes功能,也能協助分析重複加載的類,在此就不再截圖了,可以肯定的是MyAbsClass被重複加載了N多次。

最後
其實MAT工具非常的強大,上面故弄玄虛的範例代碼根本用不上MAT的其他分析功能,所以就不再描述了。其實對於OOM不只我列舉的兩種溢出錯誤,還有多種其他錯誤,但我想說的是,對於perm gen,如果實在找不出問題所在,建議使用JVM的-verbose參數,該參數會在後臺打印出日誌,可以用來查看哪個class loader加載了什麼類,例:“[Loaded org.rosenjiang.test.MyAbsClass from org.rosenjiang.test.MyClassLoader]”。
全文完。


參考資料
memoryanalyzer Blog
java類加載器體系結構
ClassLoader

發佈了61 篇原創文章 · 獲贊 25 · 訪問量 57萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章