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

 http://www.blogjava.net/rosen/archive/2010/06/13/323522.htmlphp

 

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


前言

使用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,這裏不得不囉嗦下已編譯類的結構,包括:
    一、修飾符(例如public、private)、類名、父類名、接口和annotation部分。
    二、類成員變量聲明,包括每一個成員的修飾符、名字、類型和annotation。
    三、方法和構造函數描述,包括修飾符、名字、返回和傳入參數類型,以及annotation。固然還包括這些方法或構造函數的具體Java字節碼。
    四、常量池(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 " ,
            
null new  Integer( - 1 )).visitEnd();
            cw.visitField(Opcodes.ACC_PUBLIC  +  Opcodes.ACC_FINAL  +  Opcodes.ACC_STATIC,  " EQUAL " " I " ,
            
null new  Integer( 0 )).visitEnd();
            cw.visitField(Opcodes.ACC_PUBLIC  +  Opcodes.ACC_FINAL  +  Opcodes.ACC_STATIC,  " GREATER " " I " ,
            
null new  Integer( 1 )).visitEnd();
            cw.visitMethod(Opcodes.ACC_PUBLIC  +  Opcodes.ACC_ABSTRACT,  " absTo " ,
            
" (Ljava/lang/Object;)I " null null ).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]」。
全文完。

轉載於:https://www.cnblogs.com/wangtianxj/archive/2010/10/19/1855751.htmljava