天天看點

使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)

前言

一開始不得不說說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

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 

使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)

heap dump file created [78233961 bytes in 1.995 secs]

exception in thread "main" java.lang.outofmemoryerror: java heap space

使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)

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即可。稍後會看到下圖所示的界面。

使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)

mat工具分析了heap dump後在界面上非常直覺的展示了一個餅圖,該圖深色區域被懷疑有記憶體洩漏,可以發現整個heap才64m記憶體,深色區域就占了99.5%。接下來是一個簡短的描述,告訴我們main線程占用了大量記憶體,并且明确指出system class loader加載的"java.lang.thread"執行個體有記憶體聚集,并建議用關鍵字"java.lang.thread"進行檢查。是以,mat通過簡單的兩句話就說明了問題所在,就算使用者沒什麼處理記憶體問題的經驗。在下面還有一個"details"連結,在點開之前不妨考慮一個問題:為何對象執行個體會聚集在記憶體中,為何存活(而未被gc)?是的——strong

ref,那麼再走近一些吧。

使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)

點選了"details"連結之後,除了在上一頁看到的描述外,還有shortest paths to the accumulation point和accumulated objects部分,這裡說明了從gc root到聚集點的最短路徑,以及完整的reference chain。觀察accumulated

heap為何是16,因為對象頭是8位元組,成員變量int是4位元組、string引用是4位元組,故總共16位元組。

使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)

接着往下看,來到了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

public class oompermtest {

        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 

使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)

heap dump file created [121273334 bytes in 2.845 secs]

exception in thread "main" java.lang.outofmemoryerror: permgen space

使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)

倒是在思考如何把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是一個包含類中出現的數字、字元串、類型常量的數組。

使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)

已編譯類和原來的類源碼差別在于,已編譯類隻包含類本身,内部類不會在已編譯類中出現,而是生成另外一個已編譯類檔案;其二,已編譯類中沒有注釋;其三,已編譯類沒有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表示,具體參考下圖。

使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)

下面的代碼中會使用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感興趣。首先是抽象類原型:

 * myabsclass class

public abstract class myabsclass {

    int less = -1;

    int equal = 0;

    int greater = 1;

    abstract int absto(object o);

其次是自定義類加載器,實在沒法,classloader的defineclass方法都是protected的,要加載位元組數組形式(因為tobytearray了)的類隻有繼承一下自己再實作。

 * myclassloader class

public class myclassloader extends classloader {

    public class defineclass(string name, byte[] b) {

        return defineclass(name, b, 0, b.length);

最後是測試類。

import java.util.arraylist;

import java.util.list;

import org.objectweb.asm.classwriter;

import org.objectweb.asm.opcodes;

    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();

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

dumping heap to java_pid3023.hprof 

使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)

heap dump file created [92593641 bytes in 2.405 secs]

使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)
使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)

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

使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)

更進一步分析,點選上圖中紅框線圈起來的按鈕,選擇java basics——class loader explorer功能。打開後能看到下圖所示的界面,第一列是class loader名字;第二列是class loader已定義類(defined classes)的個數,這裡要說一下已定義類和已加載類(loaded

classes)了,當需要加載類的時候,相應的class loader會首先把請求委派給父class loader,隻有當父class loader加載失敗後,該class loader才會自己定義并加載類,這就是java自己的“雙親委派加載鍊”結構;第三列是class loader所加載的類的執行個體數目。

使用Memory Analyzer tool(MAT)分析記憶體洩漏(二)

在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]”。

全文完。

參考資料

<a target="_blank" href="http://dev.eclipse.org/blogs/memoryanalyzer/">memoryanalyzer blog</a>

<a target="_blank" href="http://kenwublog.com/structure-of-java-class-loader">java類加載器體系結構</a>

<a target="_blank" href="http://mindprod.com/jgloss/classloader.html">classloader</a>