Eclipse-MAT工具


MAT(Memory Analyzer Tool)工具是一款功能强大的Java堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。

MAT是基于Eclipse开发的,不仅可以单独使用,还可以作为插件形式嵌入Eclipse中使用。是一款免费的性能分析工具,使用起来非常方便。

官网下载地址:

https://www.eclipse.org/mat/downloads.php

获取堆dump文件

dump文件内容

MAT可以分析heap dump文件。在进行内存分析时,只要获得了反映当前设备内存映像的hprof文件,通过MAT打开就可以直观地看到当前的内存信息。

一般来说,这些内存信息包含:

  • 所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值。
  • 所有的类信息,包括classloader、类名称、父类、静态变量等。
  • GCRoot到所有的这些对象的引用路径。
  • 线程信息,包括线程的调用栈以及此线程的线程局部变量(TLS)

MAT优缺点

缺点:MAT不是一个万能工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如:Sun,HP,SAP所采用的HPROF二进制堆存储文件,以及IBM的PHD堆存储文件等都能被很好的解析。

优点:最吸引人的还是能够快速为开发人员生成内存泄漏表,方便定位问题和解析问题。虽然MAT有如此强大的功能,但是内存分析也没有简单到一键完成的程度,很多内存问题还是需要我们从MAT展现给我们的信息当中通过经验和直觉来判断才能发现。

获取dump文件

方法一:通过 jmap 工具生成,可以生成任意一个java进程的dump文件;

方法二:通过配置JVM参数生成。

  • 选项"-XX:+HeapDumpOrOutOfMemoryError""-XX:+HeapDumpBeforeFullGC"
  • 选项 -XX:HeapDumpPath 所代表的含义就是当程序出现OutofMemory时,将会在相应的目录下生成一份dump文件。如果不指定选项 "XX:HeapDumpPath"则在当前目录下生成dump文件。

对比:考虑到生产环境中几乎不可能在线对其进行分析,大都是采用离线分析,因此使用 jmap + MAT 工具是最常见的组合。

方法三:使用VisualVM 可以导出堆dump文件。

方法四:使用MAT既可以打开一个已有的堆快照,也可以通过MAT直接从活动Java程序中导出堆快照。使用该功能将借助 jps 列出当前正在运行的Java进程,以供选择并获取快照。

分析堆dump文件

  • Leak Suspects Report(泄漏疑点报告)

自动检测堆dump文件查看哪些是可疑内存泄漏的疑点,报告里说明哪些对象还存活以及为什么这些对象没有被垃圾回收收集回收。

  • Component Report(组件报告)

分析一系列对象的集合,找到可疑的内存空间,例如重复字符串,空集合,finalizer,弱引用。

  • Re-open previously run reports(重新打开之前运行过的报告)

之前的运行过的报告在与dump同一个目录的zip文件里。

Overview概述

histogram直方图

展示了各个类的实例数目以及这些实例的Shallowheap 或 Retainedheap的总和。

thread overview线程概述

  • 查看系统中的Java线程

  • 查看局部变量的信息

  • 获得对象相互引用的关系

    with outgoing references

    with incoming references

浅堆与深堆

浅堆shallow heap

浅堆是指一个对象所消耗的内存。在32位系统中,一个对象引用会占据4个字节,一个int类型会占据4个字节,long型变量会占据8个字节,每个对象头需要占用8个字节。根据堆快照格式不同,对象的大小可能会向8字节进行对齐。

以String为例:2个int值共占8字节,对象引用占用4字节,对象头8字节,合计20字节,向8字节对齐,故占24字节。(JDK7中)

int hash32 0
int hash 0
ref value Class xxx

这24字节为String对象的浅堆大小。它与String的value实际取值无关,无论字符串长度如何,浅堆大小始终是24字节。

深堆retained heap

保留集(Retained Set)

对象A的保留集指当对象A被垃圾回收后,可以被释放的所有对象集合(包括对象A本身),即对象A的保留集可以被认为是只能通过对象A被直接间接访问到的所有对象的集合。通俗地说,就是指仅被对象A所持有的对象的集合。

对象S1、S2的value属性值都指向字符串常量池的"Hello",如果S2对象value属性没有指向常量池的"Hello",在回收对象S1时,因"Hello"只能由S1直接或间接访问到,所以回收了S1同时也应该计算常量池中"Hello",它属于S1的保留集。如果S2对象value属性指向常量池常量,回收S1时,常量池中"Hello"不应该算在S1的保留集内。

深堆是指对象的保留集中所有的对象的浅堆大小之和。

⚠注意:浅堆指对象本身占用的内存,不包括其内部引用对象的大小。一个对象的深堆只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间。

补充:对象实际大小

另外一个常用的概念是对象的实际大小,这里,对象的实际大小定义为一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小。与深堆相比,似乎这个在日常开发中更为直观和被人接受,但实际上,这个概念和垃圾回收无关

下图显示了一个简单的对象引用关系图,对象A引用了C和D,对象B引用了C和E。那么对象A的浅堆大小只是A本身,不含C和D,而A的实际大小为A、C、D三者之和。而A的深堆大小为A与D之和,由于对象C还可以通过对象B访问到,因此不在对象A的深堆范围内。

理解Retained Size练习

上图中,GC Roots直接引用了A和B两个对象。

A对象的深堆大小Retained Size=A对象的Shallow Size。

B对象的深堆大小Retained Size=B对象的Shallow Size + C对象的Shallow Size。

如果GC Roots不引用D对象呢?

A对象的深堆大小Retained Size=A对象的Shallow Size。

B对象的深堆大小Retained Size=B对象的Shallow Size + C对象的Shallow Size + D对象的Shallow Size。

案例分析:StudentTrace

示例代码:

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

/**
 * 有一个学生浏览网页的记录程序,它将记录 每个学生访问过的网站地址。
 * 它由三个部分组成:Student、WebPage和StudentTrace三个类
 * 
 * VM options配置: -XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=d:\student.hprof
 */
public class StudentTrace {
    static List<WebPage> webpages = new ArrayList<WebPage>();

    public static void createWebPages() {
        for (int i = 0; i < 100; i++) {
            WebPage wp = new WebPage();
            wp.setUrl("http://www." + Integer.toString(i) + ".com");
            wp.setContent(Integer.toString(i));
            webpages.add(wp);
        }
    }

    public static void main(String[] args) {
        createWebPages();//创建了100个网页
        //创建3个学生对象
        Student st3 = new Student(3, "Tom");
        Student st5 = new Student(5, "Jerry");
        Student st7 = new Student(7, "Lily");

        for (int i = 0; i < webpages.size(); i++) {
            if (i % st3.getId() == 0)
                st3.visit(webpages.get(i));
            if (i % st5.getId() == 0)
                st5.visit(webpages.get(i));
            if (i % st7.getId() == 0)
                st7.visit(webpages.get(i));
        }
        webpages.clear();
        System.gc();

    }
}

class Student {
    private int id;
    private String name;
    private List<WebPage> history = new ArrayList<>();

    public Student(int id, String name) {
        super();
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<WebPage> getHistory() {
        return history;
    }

    public void setHistory(List<WebPage> history) {
        this.history = history;
    }

    public void visit(WebPage wp) {
        if (wp != null) {
            history.add(wp);
        }
    }
}


class WebPage {
    private String url;
    private String content;

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

Full GC前导出dump文件配置

线程概述浅堆大小分析:

也可以使用with outgoing referenceswith incoming references查看WebPage类被那些类所引用。被多个Student类所引用的WebPage类,在当前Student类被回收时不能被回收,所以每个Student类深堆大小是不同的。

支配树

支配树(Dominator Tree),概念源自图论。MAT提供了一个称为支配树的对象图。支配树体现了对象实例间的支配关系。在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。如果对象A是离对象B最近的一个支配对象,则认为对象A为对象B的直接支配者。支配树是基于对象间的引用图所建立的,它有以下基本性质:

  • 对象A的子树(所有被对象A支配的对象集合)表示对象A的保留集(retained set),即深堆。
  • 如果对象A支配对象B,那么对象A的直接支配者也支配对象B。
  • 支配树的边与对象引用图的边不直接对应。

如下图所示:左图表示对象引用图,右图表示左图所对应的支配树。对象A和B由根对象直接支配,由于在到对象C的路径中,可以经过A,也可以经过B,因此对象C的直接支配者也是根对象。对象F与对象D相互引用,因为到对象F的所有路径必然经过对象D,因此,对象D是对象F的直接支配者。而到对象D的所有路径中,必然经过对象C,即使是从对象F到对象D的引用,从根节点出发,也是经过对象C的,所以,对象D的直接支配者为对象C。

同理,对象E支配对象G。到达对象H的可以通过对象D,也可以通过对象E,因此对象D和E都不能支配对象H,而经过对象C既可以到达D也可以到达E,因此对象C为对象H的直接支配者。

MAT工具栏打开对象支配树:

posted @ 2025-05-04 12:53  Lz_蚂蚱  阅读(414)  评论(0)    收藏  举报