利用MAT进行内存泄露分析

上一篇 / 下一篇  2018-04-08 09:39:52 / 个人分类:MAT内存分析

 转载:https://blog.csdn.net/yxz329130952/article/details/50288145

MAT简介

MAT是一款非常强大的内存分析工具,在Eclipse中有相应的插件,同时也有单独的安装包。在进行内存分析时,只要获得了反映当前设备内存映像的hprof文件,通过MAT打开就可以直观地看到当前的内存信息。一般说来,这些内存信息包含:

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

MAT支持将这些信息通过我们需要的方式归纳和显示,这样,整个内存信息就一览无余地显示在了我们的面前。 
虽然MAT有如此强大的功能,但是内存分析也没有简单到一键完成的程度,很多内存问题还是需要我们从MAT展现给我们的信息当中通过经验和直觉来判断才能发现。下面,我们就先介绍一下MAT的一些功能。

Overview

用MAT打开一个hprof文件后一般会进入如下的overview界面,或者和这个界面类似的leak suspect界面,overview界面会以饼图的方式显示当前消耗内存最多的几类对象,可以使我们对当前内存消耗有一个直观的印象。但是,除非你的程序内存泄漏特别明显或者你正好在生成hprof文件之前复现了程序的内存泄漏场景,你才可能通过这个界面猜到程序出问题的地方。 
overview

Dorminator Tree(支配树)

支配树可以直观地反映一个对象的retained heap,这里我们首先要了解两个概念,shallow heap和retained heap:

  • shallow heap:指的是某一个对象所占内存大小。
  • retained heap:指的是一个对象的retained set所包含对象所占内存的总大小。

retained set指的是这个对象本身和他持有引用的对象和这些对象的retained set所占内存大小的总和,用官方的一张图来解释如下:

retained set

虽然说MAT定义的支配树和上图中的引用树稍有不同,但是二者的本质都可以反映一个对象如果被回收,有多少内存可以同时得到释放,只不过支配树的定义下可以更精确地反映这个数量。如果你感兴趣,可以继续看我之后的对支配树的概念解释,如果一时理解不过来可以跳过,把支配树简单想象成引用树即可。 
实际上支配树就是从引用树演化而来。所谓支配,例如x对象支配y对象,就是在引用树当中,一条到y的路径必然会经过x;所谓直接支配,例如x直接支配y,指的是在所有支配y的对象中,x是y最近的一个对象。支配树就是反映的这种直接支配关系,在支配树种,父节点必然是子节点的直接支配对象,下图就是一个从引用树到支配树的转换示意图:

dorminator tree

上面这幅图右边就是从左边转换而来形成的支配树,这里特别对C节点形成的子树做一下说明。CDE及DF、EG的关系就不用多说,从引用树和我们刚才对支配概念的阐述可以直接得出结论,关键是H节点,H节点在支配树种既不是D的子节点也不是E的子节点是因为,在引用树当中,能够到达H的路径有两条,DEFG节点都不是到达它的路径上必须经过的节点,符合这个条件的只有C节点,因此在支配树中H是C的子节点。现在仔细观察支配树可以发现,一个节点的子树正好就是这个节点对应的retained set。比如,如果D节点被回收,那么就可以释放DF对象所占的内存,但是H所占的内存还不能得到释放,因为可能G还在引用着H,这也是为什么在支配树中H是C节点的子节点的原因,只有C节点被回收了才一定能够回收H节点所占的内存。 
说了这么多,MAT中Dorminator Tree到底能够用到什么上面?我认为,它主要可以用于诊断一个对象所占内存为什么会不断膨胀,一个对象膨胀,就说明它对应到支配树中的子树就越来越庞大,只要分析这个对象对应的子树,确定那些对象是不应该出现在子树中就可以对问题手到病除。下面举一个实例进行简单分析。 
android中ListView对象是可以不断对其子view进行服用来达到提高内存使用效率的目的,ListView的这一特性依赖于List Adapter的getView的实现,我们通过如下代码来故意使ListView无法服用其子view,从而模拟对象膨胀的情况,代码如下:

public class LeakedListviewExample extends Activity {
    private ListView mLeakedListview;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.leaked_listview);
        mLeakedListview = (ListView) findViewById(R.id.leaked_listview);
        mLeakedListview.setAdapter(new LeakedAdapter());
    }
    private class LeakedAdapter extends BaseAdapter {
        @Override
        public int getCount() {
            return 1000;
        }
        @Override
        public Object getItem(int position) {
            return String.valueOf(position);
        }
        @Override
        public long getItemId(int position) {
            return position;
        }
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            // 不复用convertView,每次都new一个新的对象
            TextView leakedView = new TextView(LeakedListviewExample.this);
            leakedView.setText(String.valueOf(position));
            return leakedView;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

在运行代码的设备多次上下滑动这个ListView后,可以观察这个应用程序所占内存越来越大,每次手动GC后效果也不明显,说明出现了内存泄漏的现象,于是我们通过MAT dump出内存,切换到Dorminator Tree后可以看到如下的情况:

leaked dorminator tree

首先就可以看到ListView的shallow heap和retained heap是非常不成比例的,这说明这个ListView持有了大量对象的引用,正常情况下ListView是不会出现此现象。于是我们根据retained heap的大小逐步对ListView的支配树进行展开可以发现,Recycle Bin下面持有大量的TextView的引用,数量高达2500多个,而在正常情况下,ListView会复用Recycle Bin当中type类型相同的view,其中的对象个数不可能如此庞大,因此,从这里就可以断定ListView使用不当出现了内存泄漏。

Histogram

以直方图的方式来显示当前内存使用情况可能更加适合较为复杂的内存泄漏分析,它默认直接显示当前内存中各种类型对象的数量及这些对象的shallow heap和retained heap。结合MAT提供的不同显示方式,往往能够直接定位问题,还是以上面的代码为例,当我们切换到histogram视图下时,可以看到:

histogram

根据retained heap进行排序后可见,TextView存在2539个对象,消耗的内存也比较靠前,由于TextView属于布局中的组件,一般来说应用程序界面不能复杂到有这么多的TextView的存在,因此这其中必定存在问题。那么又如何来确定到底是谁在持有这些对象而导致内存没有被回收呢?有两种方式,第一就是右键选择List Objects -> with incoming reference,这可以列出所有持有这些TextView对象的引用路径,如图

incoming reference

从中可以看出几乎所有的TextView对象都被ListView持有,因此基本可以断定出现了ListView没有复用view的情况;第二种方式就更直接粗暴,但是非常有效果,那就是借助我们刚才说的dorminator tree,右键选择Immediate Dorminator后就可以看到如下结果

immediate dorminator

从中可以看到,有2508个TextView对象都被ListView的RecycleBin持有,原因自然就很明确了。

MAT的查询选项

上面主要介绍的是MAT的两个结果分析界面,其实,配合不同的查询选项,可以将它们的威力最大化。MAT中常用的查询选项集合都在顶部以图标的方式进行了显示

queries tool bar

从左到右依次为:显示Overview视图,显示Histogram视图,显示Dorminator Tree视图,使用OQL语言对结果进行查询,显示线程信息,结果智能分析,自定义查询选项集合,根据地址查询对象,结果分组,计算retained heap等。前三项就不再多说,分别对应之前我们介绍的三个结果分析结果显示方式,剩余的我挑选一些常用的选项结合一些实例进行说明。

OQL(Object Query Language)

在MAT中支持对象查询语句,这是一个类似于SQL语句的查询语言,能够用来查询当前内存中满足指定条件的所有的对象。OQL将内存中的每一个不同的类当做一个表,类的对象则是这表中的一条记录,每个对象的成员变量则对应着表中的一列。 
OQL的基本语法和SQL相似,语句的基本组成如下

SELECT * FROM [ INSTANCEOF ] <class name="name"> [ WHERE <filter-expression> ] </filter-expression></class>
  • 1
  • 2

所以,当你输入

select * from instanceof android.app.Activity
  • 1
  • 2

的时候,就会将当前内存中所有Activity及其之类都显示出来。但是OQL也有一些特别的用法。下面简单介绍一下OQL各部分的基本组成(更详细的可以参看这里)。

FROM部分

首先是OQL的from部分,指明了需要查询的对象,可以是如下的这些类型:

  • 一个类的完整名称,例如

    select * from com.example.leakdemo.LisenerLeakedActivity
    • 1
    • 2
  • 正则表达式,例如

    SELECT * FROM "java\.lang\..*"
    • 1
    • 2
  • 基本类型数组或者对象数组,例如int[]或者java.io.File[]

    select * from java.io.File[]
    • 1
    • 2
  • 对象的地址,例如0x2b7468c8

    select * from 0x2b7468c8, 0x2b7468dd
    • 1
    • 2
  • 对象的id,例如20815

    select * from 66888
    • 1
    • 2
  • 甚至可以是另外一个OQL的查询结果,以实现级联查询。

    SELECT * FROM (SELECT * FROM java.lang.Class c WHERE c implements org.eclipse.mat.snapshot.model.IClass )
    • 1
    • 2

from部分还可以用如下两个关键词进行修饰

  • INSTANCEOF : 用来包含查询指定类的子类进行查询,否则只查询和from部分完全匹配的类,例如:

    SELECT * FROM INSTANCEOF android.app.Activity
    • 1
    • 2
  • OBJECTS : 修饰后最多只会返回一条结果,指明查询结果对应的类信息,而不是对象信息,例如:

    SELECT * FROM OBJECTS com.example.leakdemo.LisenerLeakedActivity
    • 1
    • 2

    返回的就是LisenerLeakedActivity对应的类的信息,而不是内存中存在的ListenerLeakedActivity对象。

from部分还能为查询指定别名,以便提高可读性。例如

SELECT result.mType FROM OBJECTS com.example.leakdemo.LisenerLeakedActivity result
  • 1
  • 2

这里的result即是我们指定的别名,在我们不指定的情况下OQL默认别名为空,如下语句和上面语句效果是相同的

SELECT .mType FROM OBJECTS com.example.leakdemo.LisenerLeakedActivity
  • 1
  • 2
SELECT部分

这部分和WHERE部分只最灵活也是最复杂的部分,但是大部分情况下我认为在这里使用“*”最好,因为一般这样我们的结果里面的信息是最全。不过如果你不想被结果中的其他信息干扰,可以自定义这部分的内容来实现。 
OQL可以通过自定义select来实现对内存中对象或者类的各种属性及方法结果等进行查询,这主要通过以下三种表达方式来实现。

  • 访问查询对象的属性的表达式为:

    [ <alias>. ] <field> . <field>. <field>
    • 1
    • 2

    其中alias代表别名,是在from部分中定义的,可以省略;field就是对象中定义的属性。这种方式能够访问到查询对象的属性值,例如:

    SELECT result.mType FROM OBJECTS com.example.leakdemo.LisenerLeakedActivity result
    • 1
    • 2

    能够展现出LisenerLeakedActivity.mType组成的查询结果。

  • 查询访问Java Bean及该对象在MAT中的属性,需要的表达式为:

    [ <alias>. ] @<attribute>
    • 1
    • 2

    通过这种方式能够查询到的内容比较多,我们可以通过使用MAT中对OQL的自动补全功能(点这里查看)来选择需要查询的内容,例如:

TAG:

 

评分:0

我来说两句

日历

« 2024-03-27  
     12
3456789
10111213141516
17181920212223
24252627282930
31      

数据统计

  • 访问量: 73533
  • 日志数: 55
  • 建立时间: 2016-04-19
  • 更新时间: 2020-09-23

RSS订阅

Open Toolbar