JVM MAT使用分析详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ZYC88888/article/details/79975194

MAT简介

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

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

MAT支持将这些信息通过我们需要的方式归纳和显示,这样,整个内存信息就一览无余地显示在了我们的面前。 

虽然MAT有如此强大的功能,但是内存分析也没有简单到一键完成的程度,很多内存问题还是需要我们从MAT展现给我们的信息当中通过经验和直觉来判断才能发现。下面,我们就先介绍一下MAT的一些功能。

调用jdk工具jps查看当前的java进程

C:/>jps

3504 Jps

3676 Bootstrap

3496 org.eclipse.equinox.launcher_1.0.201.R35x_v20090715.jar

调用jmap工具得到信息

C:/>jmap -dump:format=b,file=heap.bin 3676

Dumping heap to C:/heap.bin ...

Heap dump file created

这时,我们的C盘根目录,就生成了heap.bin文件,用eclipse的file---->open打开这个文件,首先是一个启动图:

这里可以选择查看

1、内存泄露报表,自动检查可能存在内存泄露的对象,通过报表展示存活的对象以及为什么他们没有被垃圾收集;

2、对象报表,对可颖对象的分析,如字符串是否定义重了,空的collection、finalizer以及弱引用等


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;
        }
    }
}   

在运行代码的设备多次上下滑动这个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>

所以,当你输入

select * from instanceof android.app.Activity

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

FROM部分

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

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

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

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

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

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

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

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

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

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

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

    SELECT * FROM OBJECTS com.example.leakdemo.LisenerLeakedActivity

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

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

SELECT result.mType FROM OBJECTS com.example.leakdemo.LisenerLeakedActivity result

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

SELECT .mType FROM OBJECTS com.example.leakdemo.LisenerLeakedActivity
SELECT部分

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

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

    [ <alias>. ] <field> . <field>. <field>

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

    SELECT result.mType FROM OBJECTS com.example.leakdemo.LisenerLeakedActivity result

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

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

    [ <alias>. ] @<attribute> 

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

    SELECT result.@clazz AS "class", result.@displayName AS "display name" FROM com.example.leakdemo.LisenerLeakedActivity result  

    就可以得到对象对应的class信息和对象在heap dump中的显示名称。

  • 查询访问一些底层对象的Java方法的返回结果。MAT可以通过反射来调用当前查询对象的底层类的Java方法,从而得到这些方法的执行结果。其表达式为:

    [ <alias> . ] @<method>( [ <expression>, <expression> ] ) ...

    注意method后面的括号是必不可少的,否则OQL会认为这是一个属性名称。”@”符号在新版本的OQL已经可以不需要了,例如:

    SELECT result.@clazz.hasSuperClass(), result.getField("mType") FROM com.example.leakdemo.LisenerLeakedActivity result 

    可以得到当前对象是否有基类,以及对象中的mType属性的值是多少。

不仅如此,select部分还支持很多有用的内建函数,这里就不一一介绍了,大家可以戳这里查看。 
关于select部分的更多信息还可以到这里具体查看。

WHERE部分

灵活设置where参数可以为我们排除很大一部分的干扰信息,这部分对于属性和方法的支持和select部分相同,支持的运算符号和关键字有:

<=, >=, >, <, [ NOT ] LIKE, [ NOT ] IN, IMPLEMENTS, =, !=, AND, OR

相信大家都清楚这些关键值的意义,举一个简单例子

SELECT * FROM com.example.leakdemo.LisenerLeakedActivity result where result.mType = 1

上面那个查询语句可以查询到所有LisenerLeakedActivity.mType为1的对象。关于where部分的相信介绍可以参考这里。 
OQL可以用来排查缓慢内存泄漏的情况,这种形式的内存泄漏只有程序长时间运行才会造成OOM,在排查阶段由于泄漏不严重而不能够在Dominator Tree或者Histogram中明显表现出来,但是如果你清楚在当前阶段程序中的一些重要对象的大概数量的话,则可以通过OQL来查询验证。这种方式对于在Android中排查Activity泄漏十分有用,例如有下面的代码:

public class LisenerLeakedActivity extends Activity {
    private static final String TYPE_KEY = "type_key";
    private DemoListener mListener;
    private int mType = -1;
    public static void startListenerLeakedActivity(Context context, int actiivtyType) {
        Intent intent = new Intent(context, LisenerLeakedActivity.class);
        intent.putExtra(TYPE_KEY, actiivtyType);
        context.startActivity(intent);
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mType = getIntent().getIntExtra(TYPE_KEY, -1);
        mListener = new InnerListener();
        DemoListenerManager.getInstance().registerListener(mListener);
    }
    public int getActivityType() {
        return mType;
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 故意制造泄漏
//        if (null != mListener) {
//            DemoListenerManager.getInstance().unregisterListener(mListener);
//        }
    }
    private class InnerListener implements DemoListener {
        @Override
        public void nothingToHandle() {
            // nothing to do
        }
    }
}

上面Activity会在onCreate中向DemoListenerManager注册一个Listener,而这个Listener是Activity的内部类,因此它持有这个Activity的引用,如果在Activity的onDestroy阶段没有及时的反注册,那么这个Activity就泄漏了。现在随着多次启动和关闭这个Activity,我们可以发现gc后的内存总是不断增长,这是一个内存泄漏的特征,于是我们dump出当前内存,但是在其中的Dorminator Tree和Histogram中都不能很明确地得出问题所在,于是我们只能按照经验使用OQL来排查。假设你对Activity的泄漏有所怀疑,通过简单输入:

select * from instanceof android.app.Activity

可以得到如下结果:

OQL

从结果当中可以看到,当前内存中存在多个ListenerLeakActivity,而按照我们的原则,离开一个Activity就应该及时销毁和释放改Activity的资源,因此这就说明了Activity的泄漏是造成泄漏的原因之一。

自定义查询选项集合

这部分的功能其实和在显示列表中点击右键得到的菜单相同,如下所示:

context menu

下面挑选比较重要的几个选项来说明。

List Objects

有两个选项:with incoming refrences和with outcoming refreences,分别会罗列出选定对象在引用树中的父节点和子节点,可以作为为什么当前对象还保留在内存中的基本依据。在平时的排查过程中,此功能还是比较常用,因为可以顺着罗列出的引用路径一步步分析内存泄漏的具体原因,例如如下是上面Demo中针对ListenerLeakedActivity的一个with incoming reference的结果,从这个结果中可以逐步展开,最后得到是因为DemoListenerManager的单例仍然持有ListenerLeakedActivity的InnerListener造成泄漏的结论。

List Objects

注意,如果你选择with incoming references得到的每个结果展开后仍然是针对选择展开的内容的with incoming references,对于with outcoming references也一样。因此,如果两个对象存在循环引用,你需要仔细分析,排除循环引用的路径。例如上图中就不能再展开引用源:ListenerLeakedActivity的mListener进行分析,因为ListenerLeakedActivity本身就和InnerListner存在相互引用关系,展开就又得到了ListenerLeakedActivity的incoming references结果,和一级列表结果是一样的,一直这样展开下去就真是子子孙孙无穷匮也了。

Show objects by class

和List Objects相似,也可以罗列出当前选中对象在引用树种的上下级关系,只是这个选项会将结果中类型相同的对象进行归类,比如结果中有多个String类型的对象,List Objects会将它们分别显示出来,而Show objects by class会将它们统一归类为java.lang.String。虽然相对来说结果显示简洁了,但是我个人在平时分析的过程中一般较少使用到这个功能。

Merge Shortest Path To GC Roots

这是快速分析的一个常用功能,它能够从当前内存映像中找到一条指定对象所在的到GC Root的最短路径。这个功能还附带了其他几个选项,这几个选项分别指明了计算最短路径的时候是否是需要排除弱引用、软引用及影子引用等,一般来说这三种类型的引用都不会是造成内存泄漏的原因,因为JVM迟早是会回收只存在这三种引用的资源的,所以在dump内存映像之前我们都会手动触发一次gc,同时在找最短引用路径的时候也会选择上exclude all phantom/weak/soft etc. references选项,排除来自这三种引用的干扰。如下是针对ListenerLeakedActivity的一次Merge Shortest Path To GC Roots的结果,从图中可以明显地看到一条从sInstance->mListener->array->ListenerLeakedActivity的引用路径,这个结果已经足以定位Demo中内存泄漏的原因了。

Merge Shortest Path To GC Roots

Java Basics

这个选项并不是很常用,这里只简单介绍一下其中的Class Loader Explorer子选项,它可以用来查看一些和选定对象的class loader相关的特性,包括这个class loader的名称、继承关系和它所加载的所有类型,如果你的程序当中应用到了自定义class loader或者osgi相关的特性,这个选项会为你分析问题提供一定程度的帮助。如下是我们通过Demo中的ListenerLeakedActivity的Class Loader Explorer可以看到如下结果。结果表明ListenerLeakedActivity是由PathClassLoader加载的,同时罗列了由这个class loader加载的其他8个类型以及这些类型目前在内存中的实例数量。

Class Loader Explorer

Java Collections

这块的功能平时确实少有用到,对其中的功能应用不太了解,大家有兴趣可以参考这里。也欢迎大家帮助我补充一下这块的功能应用。

Immidiate Dominators

这是一个非常有用的功能,它的作用就是找出选择对象在Dominator Tree中的父节点。根据我们之前对Dominator Tree的阐述,就是找到一个对此对象存在于内存中负直接责任的对象,这个方法一般和List Objects或者Merge Shortest Path To GC Roots相互结合使用,共同定位或相互佐证。因为去掉了一个节点在引用树中的一个父节点并不一定能保证这个对象会被回收,但是如果去掉了它的Immediate Dominator,那这个对象一定会被回收掉;但同时,有时候一个对象的Immidate Dominator计算出来是GC Roots,因为它处在引用树中的路径没有一个非GC Roots的共同点,这个时候我们直接用Immidiate Dominators就不太好分析,而需要借助List Objects或者Merge Shortest Path To GC Roots的功能。因此,这几个方法需要相互配合使用。

Show Retained Set

这个功能简单来说就是显示选定对象对应在Dominator Tree中的子节点集合,所有这些子节点所占空间大小对应于它的retained heap。可以用来判断当一个对象被回收的话有多少其他类型的对象会被同时回收掉,例如,我们上面的Demo中,因为InnerListener同时被ListenerLeakedActivity和DemoListenerManager所持有,因此它不在这两个对象任何一个的retained set中,而是在这两个对象的公共节点GC Roots的retained set中,但是如果我们没有向DemoListenerManager中注册过InnerListener,那么它会出现在ListenerLeakedActivity的retained set中。

结果分组

这个选项支持将列表显示的结果按照继承关系、包名或者class loader进行分组,默认情况下我们结果显示是不分组的,例如Histogram和Dominator Tree的查询结果只是将对应引用树和Dominator Tree中的各个节点罗列出来,这样就包含了很多系统中的其他我们不关注的信息,使用分组的方法就可以方便聚焦到我们关注的内容之中。例如,下面分别是我们对Demo的Histogram视图结果进行按照包名和继承关系进行分组的结果显示。从中看到按照包名排序的方式方便我们着重关注那些我们自己实现的类在内存中的情况,因为毕竟一般情况下出现泄漏问题的是我们自己的代码;而按照继承关系的方式进行分类我们可以直接看到当前内存中到底有多少个Activity的子类及其对象存在,方便我们重点排查某些类。

group by package
group by superclass

计算retained heap

在我们一开始选择Histogram或者Dominator Tree视图查询结果的时候,MAT并没有立即开始为我们计算对应各个节点的Rtained Heap大小,此时我们可以用这个功能来进行计算,以方便我们进行泄漏的判断。这个选项又包含两个算法,一个是quick approx的方式,即快速估算,这个算法特点是速度快,能够快速计算出罗列各项Retained Heap所占的最小内存,所以你用这个方式计算后看到的Rtained Heap都是带有”>=”标志的;如果你想精确地了解各项对应Retained Heap的大小,可以选择使用Calculate Precise Retained Size,但是根据当前内存映像大小和复杂度的不同,计算过程耗时也不相同,但一般都比第一种方式慢得多。

结束语

还是花了不少时间来写这篇博客,中间参考了不少其他同行的劳动成果,也不知道是否后面能否完全一一列举,但更多的是自己的总结。在写这篇博客之前自己也使用过一段MAT,但并不是每个功能都使用到了,所以如果其中有什么地方理解有误还望各位读者留言指正。 
关于Android内存泄漏检测的一个神器LeakCanary,大家可以参考我之前写的另外一篇博客

参考:

1.LeakCanary简介

目前Java程序最常用的内存分析工具应该是MAT(Memory Analyzer Tool),它是一个Eclipse插件,同时也有单独的RCP客户端,也可以通过官网的SVN下载到它的源码(具体见另一篇《compile-MAT》)并编译成jar包。LeakCanary本质上就是一个基于MAT进行Android应用程序内存泄漏自动化检测的的开源工具,通过集成这个工具代码到自己的Android工程当中就能够在程序调试开发过程中通过一个漂亮的界面(如下图)随时发现和定位内存泄漏问题,而不用每次在开发流程中都抽出专人来进行内存泄漏问题检测,极大地方便了Android应用程序的开发。

LeakCanary_result

总的来说,LeakCanary有如下几个明显优点:

  • 针对Android Activity组件完全自动化的内存泄漏检查。
  • 可定制一些行为(dump文件和leaktrace对象的数量、自定义例外、分析结果的自定义处理等)。
  • 集成到自己工程并使用的成本很低。
  • 友好的界面展示和通知。

假如你现在想集成LeakCanary到自己的工程中,那么你只需要做以下工作: 1. 导入leakcanary的jar包到自己工程(下载链接:leakcanary.zip) 2. 在4.0以上,只需要在工程的Application的onCreate函数中按照如下的方式加入一行代码:

public class ExampleApplication extends Application {
      @Override
      public void onCreate() {
        super.onCreate();
        LeakCanary.install(this);
      }
}

  4.0以下在需要进行内存泄漏监控的Activity的onDestroy方法中按如下加入代码:

protected void onDestroy() {
    super.onDestroy();
    // start watch
    HeapDump.Listener heapDumpListener =
    new ServiceHeapDumpListener(this, listenerServiceClass);
    DebuggerControl debuggerControl = new AndroidDebuggerControl();
    AndroidHeapDumper heapDumper = new AndroidHeapDumper();
    heapDumper.cleanup();
    ExcludedRefs excludedRefs = AndroidExcludedRefs.createAppDefaults().build();
    RefWatcher refWatcher = new RefWatcher(new AndroidWatchExecutor(), debuggerControl, GcTrigger.DEFAULT,
    heapDumper, heapDumpListener, excludedRefs);
}   第二种情况下,在有多个Activity需要检测的情况看起来稍显繁琐,实际上可以用以上方法实现一个基类Activity,之后需要内存泄漏检测的Activity直接继承这个基类Activity就不需要每次都重复处理oonDestroy方法了。并且以上代码只作为示例,实际上每次watch的时候并不需要重新new一个RefWatcher对象,因为这个对象是可以重复使用的。

完成了以上两个步骤后,LeakCanary就可以为你的工程服务了,这之中需要我们自己处理的工作很少,相比较我们自己手工用MAT进行内存泄漏检测而言,确实方便了很多。 ## 2.LeakCanary原理分析 ## 这么强大的工具,它是如何实现的呢,引用LeakCanary中文使用说明,它的基本工作原理如下:

  1. RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。
  2. 然后在后台线程检查引用是否被清除,如果没有,调用GC。
  3. 如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。
  4. 在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。
  5. 得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄漏。
  6. HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄漏。如果是的话,建立导致泄漏的引用链。
  7. 引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。

但事实上一切并没那么简单,LeakCanary的设计者在实现的时候实际上为我们考虑了很多细节。可以通过源码分析来走一遍一次内存泄漏检查的流程。 在一个Activity生命周期结束调用oonDestroy方法的时候会触发LeakCanary进行一次内存泄漏检查,LeakCanary开始进行检查的入口函数实际上是RefWatcher类的,watch方法,其源码如下:

public void watch(Object watchedReference, String referenceName) {

  ...

  String key = UUID.randomUUID().toString();
  retainedKeys.add(key);
  final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);

  watchExecutor.execute(new Runnable() {
      @Override
      public void run() {
          ensureGone(reference, watchStartNanoTime);
      }
  });
} 这个函数做的主要工作就是为需要进行泄漏检查的Activity创建一个带有唯一key标志的弱引用,并将这个弱引用key保存至retainedKeys中,然后将后面的工作交给watchExecutor来执行。这个watchExecutor在LeakCanary中是AndroidWatchExecutor的实例,调用它的execute方法实际上就是向主线程的消息队列中插入了一个IdleHandler消息,这个消息只有在对应的消息队列为空的时候才会去执行,因此RefWatcher的watch方法就保证了在主线程空闲的时候才会去执行ensureGone方法,防止因为内存泄漏检查任务而严重影响应用的正常执行。ensureGone的主要源码如下:

void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
    ...

    removeWeaklyReachableReferences();
    if (gone(reference) || debuggerControl.isDebuggerAttached()) {
        return;
    }
    gcTrigger.runGc();      // 手动执行一次gc
    removeWeaklyReachableReferences();
    if (!gone(reference)) {

        long startDumpHeap = System.nanoTime();
        long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

        File heapDumpFile = heapDumper.dumpHeap();
        if (heapDumpFile == null) {
            // Could not dump the heap, abort.
            Log.d(TAG, "Could not dump the heap, abort.");
            return;
        }
        long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);

        heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs,
                watchDurationMs, gcDurationMs, heapDumpDurationMs));
    }
  } 因为这个方法是在主线程中执行的,因此一般执行到这个方法中的时候之前被destroy的那个Activity的资源应该被JVM回收了,因此这个方法首先调用removeWeaklyReachableReferences方法来将引用队列中存在的弱引用从retainedKeys中删除掉,这样,retainedKeys中保留的就是还没有被回收对象的弱引用key。之后再用gone方法来判断我们需要检查的Activity的弱引用是否在retainedKeys中,如果没有,说明这个Activity对象已经被回收,检查结束。否则,LeakCanary主动触发一次gc,再进行以上两个步骤,如果发现这个Activity还没有被回收,就认为这个Activity很有可能泄漏了,并dump出当前的内存文件供之后进行分析。

之后的工作就是对内存文件进行分析,由于这个过程比较耗时,因此最终会把这个工作交给运行在另外一个进程中的HeapAnalyzerService来执行。HeapAnalyzerService通过调用HeapAnalyzer的checkForLeak方法进行内存分析,其源码如下:

public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {

    ...

    ISnapshot snapshot = null;
    try {
      snapshot = openSnapshot(heapDumpFile);  

      IObject leakingRef = findLeakingReference(referenceKey, snapshot);

      // False alarm, weak reference was cleared in between key check and heap dump.
      if (leakingRef == null) {
        return noLeak(since(analysisStartNanoTime));
      }

      String className = leakingRef.getClazz().getName();

      AnalysisResult result =
          findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, true);

      if (!result.leakFound) {
        result = findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, false);
      }

      return result;
    } catch (SnapshotException e) {
      return failure(e, since(analysisStartNanoTime));
    } finally {
      cleanup(heapDumpFile, snapshot);
    }
}

这个方法进行的第一步就是利用HAHA将之前dump出来的内存文件解析成Snapshot对象,其中调用到的方法包括SnapshotFactory的parse和HprofIndexBuilder的fill方法。解析得到的Snapshot对象直观上和我们使用MAT进行内存分析时候罗列出内存中各个对象的结构很相似,它通过对象之间的引用链关系构成了一棵树,我们可以在这个树种查询到各个对象的信息,包括它的Class对象信息、内存地址、持有的引用及被持有的引用关系等。到了这一阶段,HAHA的任务就算完成,之后LeakCanary就需要在Snapshot中找到一条有效的到被泄漏对象之间的引用路径。首先它调用findLeakTrace方法来从Snapshot中找到被泄漏对象,源码如下:

private IObject findLeakingReference(String key, ISnapshot snapshot) throws SnapshotException {

    Collection<IClass> refClasses =
        snapshot.getClassesByName(KeyedWeakReference.class.getName(), false);

    if (refClasses.size() != 1) {
      throw new IllegalStateException(
          "Expecting one class for " + KeyedWeakReference.class.getName() + " in " + refClasses);
    }

    IClass refClass = refClasses.iterator().next();

    int[] weakRefInstanceIds = refClass.getObjectIds();

    for (int weakRefInstanceId : weakRefInstanceIds) {
      IObject weakRef = snapshot.getObject(weakRefInstanceId);
      String keyCandidate =
          PrettyPrinter.objectAsString((IObject) weakRef.resolveValue("key"), 100);  
      if (keyCandidate.equals(key)) {  // 匹配key
        return (IObject) weakRef.resolveValue("referent");  // 定位泄漏对象
      }
    }
    throw new IllegalStateException("Could not find weak reference with key " + key);
  }

为了能够准确找到被泄漏对象,LeakCanary通过被泄漏对象的弱引用来在Snapshot中定位它。因为,如果一个对象被泄漏,一定也可以在内存中找到这个对象的弱引用,再通过弱引用对象的referent就可以直接定位被泄漏对象。 下一步的工作就是找到一条有效的到被泄漏对象的最短的引用,这通过findLeakTrace来实现,实际上寻找最短路径的逻辑主要是封装在PathsFromGCRootsComputerImpl这个类的getNextShortestPath和processCurrentReferrefs这两个方法当中,其源码如下:

public int[] getNextShortestPath() throws SnapshotException {
      switch (state) {
        case 0: // INITIAL
        {

          ...
        }
        case 1: // FINAL
          return null;

        case 2: // PROCESSING GC ROOT
        {
          ...
        }
        case 3: // NORMAL PROCESSING
        {
          int[] res;

          // finish processing the current entry
          if (currentReferrers != null) {
            res = processCurrentReferrefs(lastReadReferrer + 1);
            if (res != null) return res;
          }

          // Continue with the FIFO
          while (fifo.size() > 0) {
            currentPath = fifo.getFirst();
            fifo.removeFirst();
            currentId = currentPath.getIndex();
            currentReferrers = inboundIndex.get(currentId);

            if (currentReferrers != null) {
              res = processCurrentReferrefs(0);
              if (res != null) return res;
            }
          }
          return null;
        }

        default:
          ...
      }
    }


    private int[] processCurrentReferrefs(int fromIndex) throws SnapshotException {
      GCRootInfo[] rootInfo = null;
      for (int i = fromIndex; i < currentReferrers.length; i++) {
        ...
      }
      for (int referrer : currentReferrers) {
        if (referrer >= 0 && !visited.get(referrer) && !roots.containsKey(referrer)) {
          if (excludeMap == null) {
            fifo.add(new Path(referrer, currentPath));
            visited.set(referrer);
          } else {
            if (!refersOnlyThroughExcluded(referrer, currentId)) {
              fifo.add(new Path(referrer, currentPath));
              visited.set(referrer);
            }
          }
        }
      }
      return null;
    }
  }

为了是逻辑更清晰,在这里省略了对GCRoot的处理。这个类将整个内存映像信息抽象成了一个以GCRoot为根的树,getNextShortestPath的状态3是对一般节点的处理,由于之前已经定位了被泄漏的对象在这棵树中的位置,为了找到一条到GCRoot最短的路径,PathsFromGCRootsComputerImpl采用的方法是类似于广度优先的搜索策略,在getNextShortestPath中从被泄漏的对象开始,调用一次processCurrentReferrefs将持有它引用的节点(父节点),加入到一个FIFO队列中,然后依次再调用getNextShortestPath和processCurrentReferrefs来从FIFO中取节点及将这个节点的父节点再加入FIFO队列中,一层一层向上寻找,哪条路径最先到达GCRoot就表示它应该是一条最短路径。由于FIFO保存了查询信息,因此如果要找次最短路径只需要再调用一次getNextShortestPath触发下一次查找即可,其算法原理如下图所示。

LeakCanary_result

至此,主要的工作就完成了,后面就是调用buildLeakTrace构建查询结果,这个过程相对简单,仅仅是将之前查找的最短路径转换成最后需要显示的LeakTrace对象,这个对象中包括了一个由路径上各个节点LeakTraceElement组成的链表,代表了检查到的最短泄漏路径。最后一个步骤就是将这些结果封装成AnalysisResult对象然后交给DisplayLeakService进行处理。这个service主要的工作是将检查结果写入文件,以便之后能够直接看到最近几次内存泄露的分析结果,同时以notification的方式通知用户检测到了一次内存泄漏。使用者还可以继承这个service类来并实现afterDefaultHandling来自定义对检查结果的处理,比如将结果上传刚到服务器等。

以上就是对LeakCanary源码的分析,中间省略了一些细节处理的说明,但不得不提的是LeakCanary支持自定义泄漏豁对象ExcludedRefs的集合,这些豁免对象一般都是一些已知的系统泄漏问题或者自己工程中已知但又需要被排除在检查之外的泄漏问题构成的。LeakCanary在findLeakTrace方法中如果发现这个集合中的对象存在于泄漏路径上,就会排除掉这条泄漏路径并尝试寻找下一条。

阅读更多

扫码向博主提问

ZhaoYingChao88

非学,无以致疑;非问,无以广识
  • 擅长领域:
  • 大数据
  • spring
  • SOA
  • 微服务
  • linux
去开通我的Chat快问
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页