教你玩转MAT内存分析工具

MAT 简介

       MAT 工具是基于 Eclipse 平台开发的, 本身是一个 Java 程序, 是一款很好的内存分析工具, 所以如果堆快照比较大的话, 则需要一台内存比较大的分析机器, 并给 MAT 本身加大初始内存, 这个可以修改安装目录中的 MemoryAnalyzer.ini 文件。

     在MAT中,我们一般比较常用的几个小工具,如下图标识所示

 

概要

柱状图

    柱状图中,主要看每个class对应的实例数、浅堆、深堆等。如下图所示

     从这个图的展示效果看出,和“jmap -histo <pid> ”命令展示的效果很像。

     而我们大部分情况下,我们关注重点,是深堆或浅堆的incoming和outgoing两项,那么这两项指的是什么呢?

1、对象的引入(incoming references)

     通俗的来讲,就是哪些对象把当前对象引用了,那么那些对象对于当前对象来说就是一些引入对象。

2、对象的引出(outgoing references)

     通俗的来讲,就是当前对象中引用了哪些对象,那么那些对象对于当前对象来说就是一些引出对象。

     下面就使用例子进行说明,准备如下代码

class A {
    private C c1 = C.getInstance();
}
class B {
    private C c2 = C.getInstance();
}
class C {
    private static C myC = new C();
    public static C getInstance() {
        return myC;
    }
    private D d1 = new D();
    private E e1 = new E();
}
class D {
}
class E {
}
public class MATRef {
    public static void main(String[] args) throws Exception {
        A a = new A();
        B b = new B();
        Thread.sleep(Integer.MAX_VALUE);//线程休眠
    }
}

    那么这一段码的意图,可以翻译为下面的图所示

       下面我们就来分析对象C的Incoming和outgoing

        将代码使用main方法运行起来,然后使用MAT连接。

        注意MAT不单单只打开 dump 日志,也可以打开正在运行的 JVM 进程, 跟 arthas 有点类似,效果是一样的,只是一个是动态的,一个是日志导出那个时刻的。

        使用MAT连接正在运行的进程方式如下图所示

       选中如图中标识的这个进程并双出打开

        这个 outgoing references 和 incoming references 非常有用, 因为我们做 MAT 分析一般时对代码不了解, 排查内存泄漏也好, 排查问题也好, 垃圾回收中有一个很重要的概念, 可达性分析算法, 那么根据这个引入和引出, 我就可以知道这些对象的引用关系, 在 MAT 中我们就可以知道比如 A,B,C,D,E,F 之间的引用关系图, 便于做具体问题的分析。

MAT 中的浅堆与深堆

1、概念

        浅堆(shallow heap) 代表了对象本身的内存占用, 包括对象自身的内存占用, 以及“为了引用” 其他对象所占用的内存。

        深堆(Retained heap) 是一个统计结果, 会循环计算引用的具体对象所占用的内存。 但是深堆和“对象大小” 有一点不同, 深堆指的是一个对象被垃圾回收后, 能够释放的内存大小, 这些被释放的对象集合, 叫做保留集(Retained Set)

2、浅堆大小的计算

        我们知道,一个对象的大小是由三个部分组成,即对象头、实例数据、对齐填充。

        而我们在实际使用中,一个对象会引用其它的对象或一些基本的成员变量。那么这个浅堆大小的计算公式如下

shallow_size=对象头+各成员变量大小之和+对齐填充

        其中,各成员变量大小之和就是实例数据,如果存在继承的情况,需要包括父类成员的变量。

        除了对象,我们还需要知道数组类型对象的浅堆该如计算。计算体公式如下

shallow size=对象头+类型变量大小*数组长度+对齐填充, 如果是引用类型, 则是四字节或者八字节(64 位系统) 

        这里的类型变量大小*数组长度, 就是实例数据, 强调是变量不是对象本身

3、实例辨析

        下面,给出一张图,我们来看一下,A、B、D这三个对象的深堆和浅堆是怎么得出来的

        1、对于A对象而言,浅堆就是自身的大小,即大小为10;深堆的计算可就没有这么简单了,前面说过,对象的深堆是指如果对象被回收了,它所能释放出多少空间,那这么这个释放出来的空间就是深堆大小。从图中,可以看出从A出发,是有6个对象实例,而每个实例的大小是10,但是对象本身的大小是10,所以A的深堆就是70

        2、对于B对象而言,按照A对象的计算方式,不能算出,它的浅堆大小是10,而深堆大小是30

        3、对于D对象而言,如果D被回收了,它也只释放出的空间大小为10,即它自身的大小。所以它的浅堆和深堆都是10。因此,如果我们在使用MAT工具在分析时,看到浅堆和深堆的大小是一样的话,就可以断定,这个对象就没有引用其它对象了。

       下面再看一个图

      

        在这种情况下, 对象A的深堆大小将从之前的 70 减小到 40 个字节。如果对象A被垃圾回收了, 则将仅会影响C、 F 和 G 对象的引用,所以仅对象C、F和G 将被垃圾回收。另一方面, 由于H持有对B的引用,对象B、D和E将继续存在于内存中。即使 A 被垃圾回收,B、D和E也不会从内存中删除。 

        总结:我们可以看到在进行内存分析时,浅堆和深堆是两个非常重要的概念,尤其是深堆,影响着回收这个对象能够带来的垃圾回收的效果,所以在内存分析中,我们往往会去找那些深堆比较的大的对象, 尤其是那些浅堆比较小但深堆比较大的对象, 这些对象极有可能是问题对象。

内存泄露检测

       在日常开发中,我们可能会遇到内存泄露问题而不自知,这种问题将会是致命的,如果不找出其中的根原,那么这么始终是一个定时炸弹。那么对于内存泄露这一类的问题,就可以使用MAT工具进行分析。

       下面,准备一段简单的代码,如下


import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;

public class ObjectsMAT {


    static class A {
        B b = new B();
    }

    static class B {
        C c = new C();
    }

    static class C {
        List<String> list = new ArrayList<>();
    }

    static class Demo1 {
        Demo2 Demo2;

        public void setValue(Demo2 value) {
            this.Demo2 = value;
        }
    }

    static class Demo2 {
        Demo1 Demo1;

        public void setValue(Demo1 value) {
            this.Demo1 = value;
        }
    }

    static class Holder {
        Demo1 demo1 = new Demo1();
        Demo2 demo2 = new Demo2();

        Holder() {
            demo1.setValue(demo2);
            demo2.setValue(demo1);
        }


        private boolean aBoolean = false;
        private char aChar = '\0';
        private short aShort = 1;
        private int anInt = 1;
        private long aLong = 1L;
        private float aFloat = 1.0F;
        private double aDouble = 1.0D;
        private Double aDouble_2 = 1.0D;
        private int[] ints = new int[2];
        private String string = "1234";
    }

    Runnable runnable = () -> {
        Map<String, A> map = new HashMap<>();

        IntStream.range(0, 100).forEach(i -> {
            byte[] bytes = new byte[1024 * 1024];
            String str = new String(bytes).replace('\0', (char) i);
            A a = new A();
            a.b.c.list.add(str);

            map.put(i + "", a);
        });

        Holder holder = new Holder();

        try {
            //sleep forever , retain the memory
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    };

    void startThread() throws Exception {
        new Thread(runnable, "hl-thread").start();
    }

    public static void main(String[] args) throws Exception {
        ObjectsMAT objectsMAT = new ObjectsMAT();
        objectsMAT.startThread();
    }
}

       我们将这段代码使用main方法运行起来,并使用MAT进行连接

        从上图中,可以看到,存在一个"hl-thread"的线程,它的深堆和浅堆的差距非常的巨大。持有99.53%的对象,数据被一个HashMap所持有。

        那么这个就是一个泄露点,因为在代码中对线程做了标识 。所以我们在日常的开发中,如果有用到线程的,最好还是给线程取上名称,这样对排查问题有很大的帮助。

        所以, 如果是对于特别明显的内存泄漏, 在这里能够帮助我们迅速定位, 但通常内存泄漏问题会比较隐蔽, 我们需要做更加复杂的分析。

支配树视图

       支配树列出了堆中最大的对象, 第二层级的节点表示当被第一层级的节点所引用到的对象, 当第一层级对象被回收时, 这些对象也将被回收。这个工具可以帮助我们定位对象间的引用情况, 以及垃圾回收时的引用依赖关系。
       支配树视图对数据进行了归类, 体现了对象之间的依赖关系。 我们通常会根据“深堆” 进行倒序排序, 可以很容易的看到占用内存比较高的几个对象,点击前面的箭头, 即可一层层展开支配关系(依次找深堆明显比浅堆大的对象) 。

        从上图的层层分解得出。原来是“hl-thread”的深堆和浅堆比例很多(深堆比浅堆多很多,一般经验都是找那些浅堆比较小,同时深堆比较大的对象)

1、 一个浅堆非常小的 hl-thread 持有了一个非常大的深堆
2、 这个关系来源于一个 HashMap
3、 这个 map 中有对象 A, 同时 A 中引用了 B, B 中引用了 C
4、 最后找到 C 中里面有一个 ArrayList 引用了一个大数据的数组。

        经过分析,内存泄露点就在于此。

线程视图

       想要看具体的引用关系, 可以通过线程视图。 线程在运行中是可以作为 GC Roots 的。 我们可以通过线程视图展示了线程内对象的引用关系, 以及方法调用关系, 相对比 jstack 获取的栈 dump, 我们能够更加清晰地看到内存中具体的数据。

       还有另外一段是陷入无限循环,这个是相互引用导致的(进行问题排查不用被这种情况给误导了,这样的情况一般不会有问题,可达性分析算法的解决了相互引用的问题) 。

柱状图视图

       柱状图视图,可以看到除了对象的大小, 还有类的实例个数。 结合 MAT 提供的不同显示方式, 往往能够直接定位问题。 也可以通过正则过滤一些信息,我们在这里输入MAT,过滤猜测的、可能出现问题的类, 可以看到, 创建的这些自定义对象, 刚好100个对象实例,而在代码中指定就是100

        下面我们看一下对象A的引入,即A被哪些对象引用了。

 

Path To GC Roots

        从一个对象到 GC Roots 的引用链被称为 Path to GC Roots,通过分析 Path to GC Roots 可以找出 JAVA 的内存泄露问题, 当程序不在访问该对象时仍存在到该对象的引用路径(这个对象可能内存泄漏)。
        我们还是以上面的incoming模式为例

       使用这种方式, 即可在引用之间进行跳转, 方便的找到所需要的信息(这里从对象反推到了线程 hl-thread) ,也可以快速定位到有内存泄漏的问题代码。


高级功能QOL

        MAT工具支持一种类似于 SQL 的查询语言 OQL(Object Query Language) ,这个查询语言VisualVM工具也支持,不过一般情况下不用VisualVM工具。使用方法如下图

        查询A对象

           
       查询包含 java 字样的所有字符串

      当然了,QOL用还有更多的用法,可以参考网址 http://tech.novosoft-us.com/products/oql_book.htm

  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值