利用MAT进行内存泄露分析2

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

    • 1
    • 2

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

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

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

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

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

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

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

WHERE部分

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

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

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

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

上面那个查询语句可以查询到所有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
        }
    }
}
  • 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
  • 33
  • 34
  • 35
  • 36

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

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

可以得到如下结果:

OQL

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

自定义查询选项集合

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

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,大家可以参考我之前写的另外一篇博客

参考:


TAG:

 

评分:0

我来说两句

日历

« 2024-04-23  
 123456
78910111213
14151617181920
21222324252627
282930    

数据统计

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

RSS订阅

Open Toolbar