一文深度講解JVM 內存分析工具 MAT及實踐(建議收藏) 3.1 References 4.1 inspector 4.2 集合狀態

1. 前言

熟練掌握 MAT 是 Java 高手的必備能力,但實踐時大家往往需面對衆多功能,眼花繚亂不知如何下手,小編也沒有找到一篇完善的教學素材,所以整理本文幫大家系統掌握 MAT 分析工具。

本文詳細講解 MAT 衆多內存分析工具功能,這些功能組合使用異常強大,熟練使用幾乎可以解決所有的堆內存離線分析的問題。我們將功能劃分爲4類:內存分佈詳情、對象間依賴、對象狀態詳情、按條件檢索。每大類有多個功能點,本文會逐一講解各功能的場景及用法。此外,添加了原創或引用案例加強理解和掌握。

如圖所示:

爲減少對眼花繚亂的菜單的迷茫,可以通過下圖先整體熟悉下各功能使用入口,後續都會講到。

2. 內存分佈詳解及實戰

2.1 全局信息概覽

功能:展現堆內存大小、對象數量、class 數量、class loader 數量、GC Root 數量、環境變量、線程概況等全局統計信息。

使用入口:MAT 主界面 → Heap Dump Overview。

舉例:下面是對象數量、class loader 數量、GC Root 數量,可以看出 class loader 存在異常。

舉例:下圖是線程概況,可以查看每個線程名、線程的 Retained Heap、daemon 屬性等。

[圖片上傳失敗...(image-32f6a8-1611130969106)]

使用場景 全局概覽呈現全局統計信息,重點查看整體是否有異常數據,所以有效信息有限,下面幾種場景有一定幫助:

  • 方法區溢出時(Java 8後不使用方法區,對應堆溢出),查看 class 數量異常多,可以考慮是否爲動態代理類異常載入過多或類被反覆重複加載。* 方法區溢出時,查看 class loader 數量過多,可以考慮是否爲自定義 class loader 被異常循環使用。* GC Root 過多,可以查看 GC Root 分佈,理論上這種情況極少會遇到,筆者只在 JNI 使用一個存在 BUG 的庫時遇到過。* 線程數過多,一般是頻繁創建線程但無法執行結束,從概覽可以瞭解異常表象,具體原因可以參考本文線程分析部分內容,此處不展開。

2.2 Dominator tree

注:筆者使用頻率的 Top1,是高效分析 Dump 必看的功能。

功能

  • 展現對象的支配關係圖,並給出對象支配內存的大小(支配內存等同於 Retained Heap,即其被 GC 回收可釋放的內存大小)* 支持排序、支持按 package、class loader、super class、class 聚類統計

使用入口:全局支配樹: MAT 主界面 → Dominator tree。

舉例: 下圖中通過查看 Dominator tree,瞭解到內存主要是由 ThreadAndListHolder-thread 及 main 兩個線程支配(後面第2.6節會給出整體案例)。

使用場景

  • 開始 Dump 分析時,首先應使用 Dominator tree 瞭解各支配樹起點對象所支配內存的大小,進而瞭解哪幾個起點對象是 GC 無法釋放大內存的原因。* 當個別對象支配樹的 Retained Heap 很大存在明顯傾斜時,可以重點分析佔比高的對象支配關係,展開子樹進一步定位到問題根因,如下圖中可看出最終是 SameContentWrapperContainer 對象持有的 ArrayList 過大。
  • 在 Dominator tree 中展開樹狀圖,可以查看支配關係路徑(與 outgoing reference 的區別是:如果 X 支配 Y,則 X 釋放後 Y必然可釋放;如果僅僅是 X 引用 Y,可能仍有其他對象引用 Y,X 釋放後 Y 仍不能釋放,所以 Dominator tree 去除了 incoming reference 中大量的冗餘信息)。* 有些情況下可能並沒有支配起點對象的 Retained Heap 佔用很大內存(比如 class X 有100個對象,每個對象的 Retained Heap 是10M,則 class X 所有對象實際支配的內存是 1G,但可能 Dominator tree 的前20個都是其他class 的對象),這時可以按 class、package、class loader 做聚合,進而定位目標。* 下圖中各 GC Roots 所支配的內存均不大,這時需要聚合定位爆發點。

在 Dominator tree 展現後按 class 聚合,如下圖:

可以定位到是 SomeEntry 對象支配內存較多,然後結合代碼進一步分析具體原因。

在一些操作後定位到異常持有 Retained Heap 對象後(如從代碼看對象應該被回收),可以獲取對象的直接支配者,操作方式如下。

2.3 Histogram 直方圖

注:筆者使用頻率 Top2

功能

  • 羅列每個類實例的數量、類實例累計內存佔比,包括自身內存佔用量(Shallow Heap)及支配對象的內存佔用量(Retain Heap)。* 支持按對象數量、Retained Heap、Shallow Heap(默認排序)等指標排序;支持按正則過濾;支持按 package、class loader、super class、class 聚類統計,

使用入口:MAT 主界面 → Histogram;注意 Histogram 默認不展現 Retained Heap,可以使用計算器圖標計算,如下圖所示。

使用場景

  • 有些情況 Dominator tree 無法展現出熱點對象(上文提到 Dominator tree 支配內存排名前20的佔比均不高,或者按 class 聚合也無明顯熱點對象,此時 Dominator tree 很難做關聯分析判斷哪類對象佔比高),這時可以使用 Histogram 查看所有對象所屬類的分佈,快速定位佔據 Retained Heap 大頭的類。

使用技巧

  • Integer,String 和 Object[] 一般不直接導致內存問題。爲更好的組織視圖,可以通過 class loader 或 package 分組進一步聚焦,如下圖。

Histogram 支持使用正則表達式來過濾。例如,我們可以只展示那些匹配com.q.*的類。

可以在 Histogram 的某個類繼續使用 outgoing reference 查看對象分佈,進而定位哪些對象是大頭

2.4 Leak Suspects

功能:具備自動檢測內存泄漏功能,羅列可能存在內存泄漏的問題點。

使用入口:一般當存在明顯的內存泄漏時,分析完Dump文件後就會展現,也可以如下圖在 MAT 主頁 → Leak Suspects。

使用場景:需要查看引用鏈條上佔用內存較多的可疑對象。這個功能可解決一些基礎問題,但複雜的問題往往幫助有限。

舉例

  • 下圖中 Leak Suspects 視圖展現了兩個線程支配了絕大部分內存。

下圖是點擊上圖中 Keywords 中 "Details" ,獲取實例到 GC Root 的最短路徑、dominator 路徑的細信息。

2.5 Top Consumers

功能:最大對象報告,可以展現哪些類、哪些 class loader、哪些 package 佔用最高比例的內存,其功能 Histogram 及 Dominator tree 也都支持。

使用場景:應用程序發生內存泄漏時,查看哪些泄漏的對象通常在 Dump 快照中會佔很大的比重。因此,對簡單的問題具有較高的價值。

2.6 綜合案例一

使用工具項:Heap dump overview、Dominator tree、Histogram、Class Loader Explorer(見3.4節)、incoming references(見3.1節)

程序代碼

package com.q.mat;

import java.util.*;
import org.objectweb.asm.*;

public class ClassLoaderOOMOps extends ClassLoader implements Opcodes {

    public static void main(final String args[]) throws Exception {
        new ThreadAndListHolder(); // ThreadAndListHolder 類中會加載大對象

        List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
        final String className = "ClassLoaderOOMExample";
        final byte[] code = geneDynamicClassBytes(className);

        // 循環創建自定義 class loader,並加載 ClassLoaderOOMExample
        while (true) {
            ClassLoaderOOMOps loader = new ClassLoaderOOMOps();
            Class<?> exampleClass = loader.defineClass(className, code, 0, code.length); //將二進制流加載到內存中
            classLoaders.add(loader);
            // exampleClass.getMethods()[0].invoke(null, new Object[]{null});  // 執行自動加載類的方法,通過反射調用main
        }
    }

    private static byte[] geneDynamicClassBytes(String className) throws Exception {
        ClassWriter cw = new ClassWriter(0);
        cw.visit(V1_1, ACC_PUBLIC, className, null, "java/lang/Object", null);

        //生成默認構造方法
        MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);

        //生成構造方法的字節碼指令
        mw.visitVarInsn(ALOAD, 0);
        mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
        mw.visitInsn(RETURN);
        mw.visitMaxs(1, 1);
        mw.visitEnd();

        //生成main方法
        mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
        //生成main方法中的字節碼指令
        mw.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");

        mw.visitLdcInsn("Hello world!");
        mw.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
        mw.visitInsn(RETURN);
        mw.visitMaxs(2, 2);
        mw.visitEnd();  //字節碼生成完成

        return cw.toByteArray();  // 獲取生成的class文件對應的二進制流

    }
}
package com.q.mat;

import java.util.*;
import org.objectweb.asm.*;

public class ThreadAndListHolder extends ClassLoader implements Opcodes {
    private static Thread innerThread1;
    private static Thread innerThread2;
    private static final SameContentWrapperContainerProxy sameContentWrapperContainerProxy = new SameContentWrapperContainerProxy();

    static {
        // 啓用兩個線程作爲 GC Roots
        innerThread1 = new Thread(new Runnable() {
            public void run() {
                SameContentWrapperContainerProxy proxy = sameContentWrapperContainerProxy;
                try {
                    Thread.sleep(60 * 60 * 1000);
                } catch (Exception e) {
                    System.exit(1);
                }
            }
        });
        innerThread1.setName("ThreadAndListHolder-thread-1");
        innerThread1.start();

        innerThread2 = new Thread(new Runnable() {
            public void run() {
                SameContentWrapperContainerProxy proxy = proxy = sameContentWrapperContainerProxy;
                try {
                    Thread.sleep(60 * 60 * 1000);
                } catch (Exception e) {
                    System.exit(1);
                }
            }
        });
        innerThread2.setName("ThreadAndListHolder-thread-2");
        innerThread2.start();
    }
}

class IntArrayListWrapper {
    private ArrayList<Integer> list;
    private String name;

    public IntArrayListWrapper(ArrayList<Integer> list, String name) {
        this.list = list;
        this.name = name;
    }
}

class SameContentWrapperContainer {
    // 2個Wrapper內部指向同一個 ArrayList,方便學習 Dominator tree
    IntArrayListWrapper intArrayListWrapper1;
    IntArrayListWrapper intArrayListWrapper2;

    public void init() {
        // 線程直接支配 arrayList,兩個 IntArrayListWrapper 均不支配 arrayList,只能線程運行完回收
        ArrayList<Integer> arrayList = generateSeqIntList(10 * 1000 * 1000, 0);
        intArrayListWrapper1 = new IntArrayListWrapper(arrayList, "IntArrayListWrapper-1");
        intArrayListWrapper2 = new IntArrayListWrapper(arrayList, "IntArrayListWrapper-2");
    }

    private static ArrayList<Integer> generateSeqIntList(int size, int startValue) {
        ArrayList<Integer> list = new ArrayList<Integer>(size);
        for (int i = startValue; i < startValue + size; i++) {
            list.add(i);
        }
        return list;
    }
}

class SameContentWrapperContainerProxy {
    SameContentWrapperContainer sameContentWrapperContainer;

    public SameContentWrapperContainerProxy() {
        SameContentWrapperContainer container = new SameContentWrapperContainer();
        container.init();
        sameContentWrapperContainer = container;
    }
}
啓動參數:-Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/gjd/Desktop/dump/heapdump.hprof 
-XX:-UseCompressedClassPointers -XX:-UseCompressedOops

引用關係圖

分析過程

  1. 首先進入 Dominator tree,可以看出是 SameContentWrapperContainerProxy 對象與 main 線程兩者持有99%內存不能釋放導致 OOM。

先來看方向一,在 Heap Dump Overview中可以快速定位到 Number of class loaders 數達50萬以上,這種基本屬於異常情況,如下圖所示。

使用 Class Loader Explorer 分析工具,此時會展現類加載詳情,可以看到有524061個 class loader。我們的案例中僅有ClassLoaderOOMOps 這樣的自定義類加載器,所以很快可以定位到問題。

如果類加載器較多,不能確定是哪個引發問題,則可以將所有的 class loader對象按類做聚類,如下圖所示。

Histogram 會根據 class 聚合,並展現對象數量及其 Shallow Heap 及 Retained Heap(如Retained Heap項目爲空,可以點擊下圖中計算機的圖標並計算 Retained Heap),可以看到 ClassLoaderOOMOps 有524044個對象,其 Retain Heap 佔據了370M以上(上述代碼是100M左右)。

使用 incoming references,可以找到創建的代碼位置。

再來看方向二,同樣在佔據319M內存的 Obejct 數組採用 incoming references 查看引用路徑,也很容易定位到具體代碼位置。並且從下圖中我們看出,Dominator tree 的起點並不一定是 GC根,且通過 Dominator tree 可能無法獲取到最開始的創建路徑,但 incoming references 是可以的。

3. 對象間依賴詳解及實戰

3.1 References

注:筆者使用頻率 Top2

功能:在對象引用圖中查看某個特定對象的所有引用關係(提供對象對其他對象或基本類型的引用關係,以及被外部其他對象的引用關係)。通過任一對象的直接引用及間接引用詳情(主要是屬性值及內存佔用),提供完善的依賴鏈路詳情。

使用入口:目標域右鍵 → List objects → with outgoing references/with incoming references.

使用場景

  • outgoing reference:查看對象所引用的對象,並支持鏈式傳遞操作。如查看一個大對象持有哪些內容,當一個複雜對象的 Retained Heap 較大時,通過 outgoing reference 可以查看由哪個屬性引發的。下圖中 A 支配 F,且 F 佔據大量內存,但優化時 F 的直接支配對象 A 無法修改。可通過 outgoing reference 看關係鏈上 D、B、E、C,並結合業務邏輯優化中間環節,這依託 dominator tree 是做不到的。* incoming reference:查看對象被哪些對象引用,並支持鏈式傳遞操作。如查看一個大對象都被哪些對象引用,下圖中 K 佔內存大,所以 J 的 Retained Heap 較大,目標是從 GC Roots 摘除 J 引用,但在 Dominator tree 上 J 是樹根,無法獲取其被引用路徑,可通過 incoming reference 查看關係鏈上的 H、X、Y ,並結合業務邏輯將 J 從 GC Root 鏈摘除。

3.2 Thread overview

功能:展現轉儲 dump 文件是線程執行棧、線程棧引用的對象等詳細狀態,也提供各線程的 Retained Heap 等關聯內存信息。

使用入口:MAT 主頁 → Thread overview

使用場景

  • 查看不同線程持有的內存佔比,定位高內存消耗線程(開發技巧:不要直接使用 Thread 或 Executor 默認線程名避免全部混合在一起,使用線程儘量自命名方便識別,如下圖中 ThreadAndListHolder-thread 是自定義線程名,可以很容易定位到具體代碼)* 查看線程的執行棧及變量,結合業務代碼瞭解線程阻塞在什麼地方,以及無法繼續運行釋放內存,如下圖中 ThreadAndListHolder-thread 阻塞在 sleep 方法。

3.3 Path To GC Roots

功能:提供任一對象到 GC Root 的路徑詳情。

使用入口:目標域右鍵 → Path To GC Roots

使用場景:有時你確信已經處理了大的對象集合但依然無法回收,該功能能快速定位異常對象不能被 GC 回收的原因,直擊異常對象到 GC Root 的引用路徑。比 incoming reference 的優勢是屏蔽掉很多不需關注的引用關係,比 Dominator tree 的優勢是可以得到更全面的信息。

小技巧:在排查內存泄漏時,建議選擇 exclude all phantom/weak/soft etc.references 排除虛引用/弱引用/軟引用等的引用鏈,因爲被虛引用/弱引用/軟引用的對象可以直接被 GC 給回收,聚焦在對象是否還存在 Strong 引用鏈即可。

3.4 class loader 分析

功能

  • 查看堆中所有 class loader 的使用情況(入口:MAT 主頁菜單藍色桶圖標 → Java Basics → Class Loader Explorer)。* 查看堆中被不同class loader 重複加載的類(入口:MAT 主頁菜單藍色桶圖標 → Java Basics → Duplicated Classes)。

使用場景

  • 當從 Heap dump overview 瞭解到系統中 class loader 過多,導致佔用內存異常時進入更細緻的分析定位根因時使用。* 解決 NoClassDefFoundError 問題或檢測 jar 包是否被重複加載

具體使用方法在 2.6 及 3.5 兩節的案例中有介紹。

3.5 綜合案例二

使用工具項:class loader(重複類檢測)、inspector、正則檢索。

異常現象 :運行時報 NoClassDefFoundError,在 classpath 中有兩個不同版本的同名類。

分析過程

  1. 進入 MAT 已加載的重複類檢測功能,方式如下圖。

可以看到所有重複的類,以及相關的類加載器,如下圖。

  • 根據類名,在<Regex>框中輸入類名可以過濾無效信息。* 選中目標類,通過Inspector視圖,可以看到被加載的類具體是在哪個jar包裏。(本例中重複的類是被 URLClassloader 加載的,右鍵點擊 “_context” 屬性,最後點擊 “Go Into”,在彈出的窗口中的屬性 “_war” 值是被加載類的具體包位置)

4. 對象狀態詳解及實戰

4.1 inspector

功能:MAT 通過 inspector 面板展現對象的詳情信息,如靜態屬性值及實例屬性值、內存地址、類繼承關係、package、class loader、GC Roots 等詳情數據。

使用場景

  • 當內存使用量與業務邏輯有較強關聯的場景,通過 inspector 可以通過查看對象具體屬性值。比如:社交場景中某個用戶對象的好友列表異常,其 List 長度達到幾億,通過 inspector 面板獲取到異常用戶 ID,進而從業務視角繼續排查屬於哪個用戶,本里可能有系統賬號,與所有用戶是好友。* 集合等類型的使用會較多,如查看 ArrayList 的 size 屬性也就瞭解其大小。

舉例:下圖中左邊的 Inspector 窗口展現了地址 0x125754cf8 的 ArrayList 實例詳情,包括 modCount 等並不會在 outgoing references 展現的基本屬性。

4.2 集合狀態

功能:幫助更直觀的瞭解系統的內存使用情況,查找浪費的內存空間。

使用入口:MAT 主頁 → Java Collections → 填充率/Hash衝突等功能。

使用場景

  • 通過對 ArrayList 或數組等集合類對象按填充率聚類,定位稀疏或空集合類對象造成的內存浪費。* 通過 HashMap 衝突率判定 hash 策略是否合理。

具體使用方法在 4.3 節案例詳細介紹。

4.3 綜合案例三

使用工具項:Dominator tree、Histogram、集合 ratio。

異常現象 :程序 OOM,且 Dominator tree 無大對象,通過 Histogram 瞭解到多個 ArrayList 佔據大量內存,期望通過減少 ArrayList 優化程序。

程序代碼

package com.q.mat;

import java.util.ArrayList;
import java.util.List;

public class ListRatioDemo {

    public static void main(String[] args) {
        for(int i=0;i<10000;i++){
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    HolderContainer holderContainer1 = new HolderContainer();
                    try {
                        Thread.sleep(1000 * 1000 * 60);
                    } catch (Exception e) {
                        System.exit(1);
                    }
                }
            });
            thread.setName("inner-thread-" + i);
            thread.start();
        }

    }
}

class HolderContainer {
    ListHolder listHolder1 = new ListHolder().init();
    ListHolder listHolder2 = new ListHolder().init();
}

class ListHolder {
    static final int LIST_SIZE = 100 * 1000;
    List<String> list1 = new ArrayList(LIST_SIZE); // 5%填充
    List<String> list2 = new ArrayList(LIST_SIZE); // 5%填充
    List<String> list3 = new ArrayList(LIST_SIZE); // 15%填充
    List<String> list4 = new ArrayList(LIST_SIZE); // 30%填充

    public ListHolder init() {
        for (int i = 0; i < LIST_SIZE; i++) {
            if (i < 0.05 * LIST_SIZE) {
                list1.add("" + i);
                list2.add("" + i);
            }
            if (i < 0.15 * LIST_SIZE) {
                list3.add("" + i);
            }
            if (i < 0.3 * LIST_SIZE) {
                list4.add("" + i);
            }
        }
        return this;
    }
}

分析過程

  1. 使用 Dominator tree 查看並無高佔比起點。

使用 Histogram 定位到 ListHolder 及 ArrayList 佔比過高,經過業務分析很多 List 填充率很低,不會浪費內存。

查看 ArrayList 的填充率,MAT 首頁 → Java Collections → Collection Fill Ratio。

查看類型填寫 java.util.ArrayList。

從結果可以看出絕大部分 ArrayList 初始申請長度過大。

5. 按條件檢索詳解及實戰

5.1 OQL

功能:提供一種類似於SQL的對象(類)級別統一結構化查詢語言,根據條件對堆中對象進行篩選。

語法

SELECT * FROM [ INSTANCEOF ] <class_name> [ WHERE <filter-expression> ]
  • Select 子句可以使用“”,查看結果對象的引用實例(相當於 outgoing references);可以指定具體的內容,如 Select OBJECTS v.elementData from xx 是返回的結果是完整的對象,而不是簡單的對象描述信息);可以使用 Distinct 關鍵詞去重。 From 指定查詢範圍,一般指定類名、正則表達式、對象地址。* Where 用來指定篩選條件。* 全部語法詳見:OQL 語法* 未支持的核心功能:group by value,如果有需求可以先導出結果到 csv 中,再使用 awk 等腳本工具分析即可。

例子:查找 size=0 且未使用過的 ArrayList:select * from java.util.ArrayList where size=0 and modCount=0。

使用場景

  • 一般比較複雜的問題會使用 OQL,而且這類問題往往與業務邏輯有較大關係。比如大量的小對象整體佔用內存高,但預期小對象應該不會過多(比如達到百萬個),一個一個看又不現實,可以採用 OQL 查詢導出數據排查。

例如:微服務的分佈式鏈路追蹤系統,採集各服務所有接口名,共計200個服務卻採集到了200萬個接口名(一個服務不會有1萬個接口),這時直接在 List 中一個個查看很難定位,可以直接用 OQL 導出,定位哪個服務接口名收集異常(如把 URL 中 ID 也統計到接口中了)

5.2 檢索及篩選

功能:本文第二章內存分佈,第三章對象間依賴的衆多功能,均支持按字符串檢索、按正則檢索等操作。

使用場景:在使用 Histogram、Thread overview 等功能時,可以進一步添加字符串匹配、正則匹配條件過濾縮小排查範圍。

5.3 按地址尋址

功能:根據對象的虛擬內存十六進制地址查找對象。

使用場景:僅知道地址並希望快速查看對象做後續分析時使用,其餘可以直接使用 outgoing reference 瞭解對象信息。

5.4 綜合案例四

使用工具項:OQL、Histogram、incoming references

異常現象及目的 :程序佔用內存高,存在默認初始化較長的 ArrayList,需分析 ArrayList 被使用的佔比,通過數據支撐是否採用懶加載模式,並分析具體哪塊代碼創建了空 ArrayList。

程序代碼

public class EmptyListDemo {
    public static void main(String[] args) {
        EmptyValueContainerList emptyValueContainerList = new EmptyValueContainerList();
        FilledValueContainerList filledValueContainerList = new FilledValueContainerList();
        System.out.println("start sleep...");
        try {
            Thread.sleep(50 * 1000 * 1000);
        } catch (Exception e) {
            System.exit(1);
        }
    }
}

class EmptyValueContainer {
    List<Integer> value1 = new ArrayList(10);
    List<Integer> value2 = new ArrayList(10);
    List<Integer> value3 = new ArrayList(10);
}

class EmptyValueContainerList {
    List<EmptyValueContainer> list = new ArrayList(500 * 1000);

    public EmptyValueContainerList() {
        for (int i = 0; i < 500 * 1000; i++) {
            list.add(new EmptyValueContainer());
        }
    }
}

class FilledValueContainer {
    List<Integer> value1 = new ArrayList(10);
    List<Integer> value2 = new ArrayList(10);
    List<Integer> value3 = new ArrayList(10);

    public FilledValueContainer init() {
        value1.addAll(Arrays.asList(1, 3, 5, 7, 9));
        value2.addAll(Arrays.asList(2, 4, 6, 8, 10));
        value1.addAll(Arrays.asList(1, 1, 1, 1, 1, 1, 1, 1, 1, 1));
        return this;
    }
}

class FilledValueContainerList {
    List<FilledValueContainer> list = new ArrayList(500);

    public FilledValueContainerList() {
        for (int i = 0; i < 500; i++) {
            list.add(new FilledValueContainer().init());
        }
    }
}

分析過程

  1. 內存中有50萬個 capacity = 10 的空 ArrayList 實例。我們分析下這些對象的佔用內存總大小及對象創建位置,以便分析延遲初始化(即直到使用這些對象的時候纔將之實例化,否則一直爲null)是否有必要。
  2. 使用 OQL 查詢出初始化後未被使用的 ArrayList(size=0 且 modCount=0),語句如下圖。可以看出公有 150 萬個空 ArrayList,這些對象屬於浪費內存。我們接下來計算下總計佔用多少內存,並根據結果看是否需要優化。

計算 150萬 ArrayList佔內存總量,直接點擊右上方帶黃色箭頭的 Histogram 圖標,這個圖標是在選定的結果再用直方圖展示,總計支配了120M 左右內存(所以這裏點擊結果,不包含 modCount 或 size 大於0的 ArrayList 對象)。這類在選定結果繼續分析很多功能都支持,如正則檢索、Histogram、Dominator tree等等。

查看下圖 ArrayList 的具體來源,可用 incoming references,下圖中顯示了清晰的對象創建路徑。

總結

至此本文講解了 MAT 各項工具的功能、使用方法、適用場景,也穿插了4個實戰案例,熟練掌握對分析 JVM 內存問題大有裨益,尤其是各種功能的組合使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章