深入理解JVM(八)——java堆分析

飄揚的紅領巾發表於2017-09-12

    上一節介紹了針對JVM的監控工具,包括JPS可以檢視當前所有的java程式,jstack檢視執行緒棧可以幫助你分析是否有死鎖等情況,jmap可以匯出java堆檔案在MAT工具上進行分析等等。這些工具都非常有用,但要用好他們需要不斷的進行實踐分析。本文將介紹使用MAT工具進行java堆分析的案例。

記憶體溢位(OOM)的原因

我們常見的OOM(OutOfMemoryError)發生的原因不只是堆記憶體溢位,堆記憶體溢位只是OOM其中一種情況,OOM還可能發生在元空間、執行緒棧、直接記憶體

下面演示在各個區發生OOM的情況:

堆OOM

public static void main(String[] args)
    {
        List<Byte[]> list=new ArrayList<Byte[]>();
        for(int i=0;i<100;i++){
            //構造1M大小的byte數值
            Byte[] bytes=new Byte[1024*1024];
            //將byte陣列新增到list列表中,因為存在引用關係所以bytes陣列不會被GC回收
            list.add(bytes);
        }
    }

以上程式設定最大堆記憶體50M,執行:

B}GWSJNE`YB3TX61`G_WQ~W

](D6G~9X}D5B$JE@ALE`4Q3

顯然程式透過迴圈將佔用100M的堆空間,超過了設定的50M,所以發生了堆記憶體的OOM。

針對這種OOM,解決辦法是增加堆記憶體空間,在實際開發中必要的時候去掉引用關係,使垃圾回收器儘快對無用物件進行回收。

元空間OOM

public static void main(String[] args) throws Exception
    {
        for(int i=0;i<1000;i++){
            //動態建立類
            Map<Object,Object> propertyMap = new HashMap<Object, Object>();  
            propertyMap.put("id", Class.forName("java.lang.Integer"));    
            CglibBean bean=new CglibBean(propertyMap);
            //給 Bean 設定值    
            bean.setValue("id", new Random().nextInt(100));  
            //列印 Bean的屬性id
            System.out.println("id=" + bean.getValue("id"));    
        }
    }

以上程式碼透過Cglib動態建立class,設定後設資料區大小為4M:

%ORZ7@WET_R$MKU8~A$CA%7

由於程式碼迴圈建立class,大量的class後設資料,存放在後設資料區超過了設定的4M空間,因此報後設資料區OOM:

8OJX})A2Z(7T)%_W$4Q`@HE

解決該OOM的辦法是增大MaxMetaspaceSize引數值,或者乾脆不設定該引數,在預設情況元空間可使用的記憶體會受到本地記憶體的限制。

棧OOM

當建立新的執行緒時JVM會給每個執行緒分配棧記憶體,當建立執行緒過多,佔用的記憶體也就越多,這種情況下有可能發生OOM:

public static void main(String[] args) throws Exception {
        //迴圈建立執行緒
        for (int i = 0; i < 1000000; i++) {
            new Thread(new Runnable() {
                    public void run() {
                        try {
                            //執行緒sleep時間足夠長,保證執行緒不銷燬
                            Thread.sleep(200000000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            System.out.println("created " + i + "threads");
        }
    }

L$Y1U~O[ZWD6@~CIK5GA_~I

很明顯解決此OOM的辦法是減小執行緒數。

直接記憶體OOM

public static void main(String[] args) throws Exception {
        
        for (int i = 0; i < 1000000; i++) {
            //申請堆外記憶體,這個記憶體是本地的直接記憶體,並非java堆記憶體
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024*1024*1024);
            System.out.println("created "+i+" byteBuffer");
        }
    }

ByteBuffer的allocateDirect方法可以申請直接記憶體,當申請的記憶體超過的本地可用記憶體時,會報OOM:

K4Q7WUYB6Q8}]V7Q55$CCC9

解決該OOM的辦法是適當使用堆外記憶體,如有必要可顯式執行垃圾回收。(即在程式碼中執行System.gc();)

MAT工具使用

當java應用出現故障時,我們可能需要使用MAT分析問題,找出問題出現的原因,下面透過一個案例介紹MAT的使用方法:

準備:

我們事先從程式執行環境上使用jmap工具或者jvisualvm匯出一個堆快照檔案出來。

使用MAT工具開啟:

FW)8Z)LWY@$6COXYT]L@S)5

我們發現佔用記憶體最大的物件是AppClassLoader,我們知道AppClassLoader是用來載入應用的類,因此我們進一步檢視它引用的物件。

PQGEE[C2%3}58X8[IPZ1A98

下圖顯示了AppClassLoader引用的物件空間使用情況,“Shallow Heap”表示淺堆的大小,淺堆就是類自身所佔用的空間大小,也就是類本身後設資料的大小。“Retained Heap”表示深堆的大小,深堆表示該類以及它引用的其他類所佔用空間的總和,也表示該類被垃圾回收後,所能夠釋放的空間大小。(如果該類被回收了,他引用的物件會變成不可達物件因此也會被回收)

@(}%4RVBY%V]4EU[{0_2S%2

我們隨藤摸瓜,繼續檢視深堆佔用最大的物件。

{{7I%O]JG5SMRPDK5AS7ZV0

從上圖可以看出造成深堆比較大的原因是程式當中包含了一個ArrayList,他裡面包含有大量的String物件,並且每個String物件有80216位元組大小。

因此針對這個堆的分析基本清楚了,因為程式中包括大量的String物件,而他們又在ArrayList當中,引用關係一直存在,因此無法被垃圾回收,造成OOM。

MAT其他功能說明

除了上述我們使用到的MAT功能外,還有一些功能也是經常用到的。

Histogram:顯示每個類使用情況以及佔用空間大小。

@{`LL)UT5](BIZLTYH9O_GR

上圖可以看到char[]類,有1026個物件,佔用5967480位元組的空間,透過上面的分析得出結論是String物件佔用了大部分的空間,而Stirng物件內部存放字元使用char[]來存放的,所以這裡顯示char[]的淺堆大小為5967480位元組也是可以理解的。

Thread_overview:顯示執行緒相關的資訊。

QOI~NWMMXI1R%N9J5EWSVR7

OQL:透過類似SQL語句的表示式查詢物件資訊。

V7GA8FV@HY538J207_~DI%6

上圖透過OQL語句查詢字串中匹配123的String物件。

結語

本文首先介紹了java程式中出現OOM的幾種情況,然後透過簡單的案例介紹了MAT的基本用法。

相關文章