内存泄漏检测分析工具MAT(Memory Analyzer Tool)的使用

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

工具下载地址:https://www.eclipse.org/mat/

 

  首先准备一个案例demo。上次讲了经典Handler导致的内存泄漏,今天也讲个经典例子。单例造成的内存泄漏。

 

public class MySingleton {

    private static volatile MySingleton instance;

    private Context mContext;

    private MySingleton(Context context) {
        this.mContext = context;
    }

    public static MySingleton getInstance(Context context) {
        if (instance == null) {
            synchronized (MySingleton.class) {
                if (instance == null) {
                    instance = new MySingleton(context);
                }
            }
        }
        return instance;
    }

}

使用时如果传的Activity的当前实例this进去,一旦Activity关闭后,单例仍然持有这个Activity,就会造成内存泄漏。正确的方法应该传getApplicationContext()这个Context。

 

 

public class DemoActivity extends AppCompatActivity {

    private ImageView iv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //塞一张大一点的图片,用来增大Activity的所需内存,可以更好的在MAT中观测
        iv = new ImageView(this);
        iv.setImageResource(R.drawable.demo);

        MySingleton.getInstance(this);
    }

}

 


 

 

 

 

打开Android Minitor点击里面的Dump Java Heap可以生成一个hprof文件,后缀名是heap profile的缩写。这类文件统一放在左侧的Captures窗口里。

这文件在MAT不能直接打开,所以需要在这里右键这个文件,选择Export to standard .hprof转成成标准格式。

 

先讲两个概念

关于Shallow Size和Retained Size

https://www.yourkit.com/docs/java/help/sizes.jsp 或这篇译文 http://blog.csdn.net/kingzone_2008/article/details/9083327 讲的很详细.总结就是:

Shallow Size是对象本身占据的内存的大小,不包含其引用的对象。对于常规对象(非数组)的Shallow Size由其成员变量的数量和类型来定,数组的ShallowSize由数组类型和数组长度来决定,它为数组元素大小的总和。
Retained Size是对象本身,加上可直接或间接引用到的对象的大小,其中要减去被GC Roots存在另外一条路径引用的对象。所以这也可以理解为GC之后所能回收到内存的总和。

 

关于GC Root

Java GC是自动运行自我管理的,垃圾回收当然要判断出哪些是已经不会再使用需要回收的对象。这怎么判断?就是设立一个GC Root的概念,它表示根引用集合。从这个GC Root无法达到的对象,就是没有办法再获取和使用到,也就是需要回收的。

GC Roots具体有哪些东西可以参考

http://help.eclipse.org/luna/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Fconcepts%2Fgcroots.html&cp=37_2_3

记不住那么多没关系,主要记住有“所有线程中正在运行的方法栈里指向到堆里的对象的那个引用”,“所有静态引用变量”,“所有引用类型常量”,“所有CalssLoader”这4个就行。

 

 

回到MAT使用,一般有这么几个用法。

Histogram

里面会根据类名来区分列举出表格,后面3个列是类型的对象个数,Shallow Size和Retained Size。默认是按照Shallow Size排序,也可以更改排序方式。

 

 

Dominator Tree

里面是根据对象来区分列举出表格,后面3个列是Shallow Size,Retained Size和占据的百分比。默认是按Retained Size排序。

都说是Dominator Tree,中文翻译为支配(对象)树?所以点击每一行左边的箭头会列举出这个对象所支配的所有对象。

注意是支配(对象),而不是用持有(对象)树。下面会讲这两者的区别。

可以看到这些表格窗口都可以在列表上方的搜索栏上输入关键字搜索,支持正则表达式。

 

 

List Object

在上面的表格里,对任一行右键可以调用List Object功能。两个子选项人如其名:

with outgoing references可以列出这个对象里所持有的所有对象们。

with incoming references可以列出所有持有这个对象的对象们。

 

 

Dominator Tree和With outgoing references的区别

持有就是我们一般讲的该对象持有一个另一个对象的引用。支配的意思不局限于直接持有或间接持有,而是突出一种唯一性的特点。比如A支配B,表示要经过B的引用路径里必须经过A。

关于支配的概念以下这篇文章会讲的很详细 http://book.51cto.com/art/201504/472215.htm

下面列举DemoActivity的Dominator Tree和with outgoing references两个窗口对比一下。

可以发现,明显outgoing这个窗口会有比较多的对象。看了一遍,刚好左边有的对象右边都有,说明没有间接引用却达成支配的情况。

 

 

 

Path To GC Roots

首先说明Path To GC Roots是针对Dominator Tree表格的。在Histogram里只有Merge shortest Path To GC Roots,意思是会找出最短的路径。

Path To GC Roots人如其名,意为显示出GC Roots到这个对象的所有路径。一般用exclude all phantom/weak/soft etc. references这个排除虚、弱、软引用的这个选项。因为这几种情况堆内存GC都可以回收的到,不是造成内存泄漏的原因。唯有强引用指向的对象GC回收不了。选中后出现如下图表格

这图很直白。DemoActivity对象 ←(被持有) MySingleton对象的mContext引用 ←(被持有) MySingleton对象的instance引用(因为是静态变量,GC不会回收)。

 

 

 

使用Compare Basket面板比较两个hprof的histogram文件

这里我使用了Handler导致内存泄漏的例子。Before.hprof表示使用了非静态内部类,After.hprof表示使用了静态内部类。生成hprof文件前一定要先执行Initiate GC。

在菜单栏里选择“Window”→“Navigation History”,里面能显示当前hprof文件打开的几个页面。

可以对里面的“histogram”、“dominator_tree”、“OQL执行结果”右键,选择Add to Compare Basket

比如我利用了OQL语句把Before和After的所有Activity对象搜索结果都加入了Compare Basket。

在Compare Basket里,选中两个文件,可以点击下面的红色感叹号进行对比。也可以右键后选择compare Tables,支持自定义对比参数。

对比结果出来

我App使用过程是从MainActivity中打开了3次含有Handler的SampleActivity然后退出。没有使用静态内部类的那个Before.hprof导致内存中依然存在了3个SampleActivity实例。

 

 

 

 

Leak Suspects和Top Consumers

Overview页面里的Reports标签里,提供了Leak Suspects和Top Consumers两种报告。

Leak Suspects提供了MAT工具猜测了一些内存泄漏的地方,会生成直观的饼状图。也可以方便的点击查看对应的引用路径和支配树结构。

Top Consumers分别提供了占用内存最大的几个对象,几种对象类型,几个classloader,几个包的饼状图和相应的详细数据表格。

然而在Android应用中,一般都会被BitmapDrawable$BitmapState这种垃圾信息占用着。所以一般我都不会用这两个工具。

 

 

 

 

该如何正确的查找内存泄漏

方法有很多种,这里列举几个我常用的例子

1.查看当前开发App所有对象的内存情况

可以使用Group by package功能

也可以在搜索栏里写出包名com.yao.memorytest

 

2.查看Activity的内存泄漏

输入正则表达式com\.yao\.memorytest\.[a-zA-Z0-9_]+Activity。它是由包名开头,Activity结尾组成。专注搜索Activity对象。(会把Activity的内部类也查出来,这个正则表达式加结束符$会失效)

所以测试流程可以是这样:

①假设app首页是MainActivity,我们打开app的MainActivity后,把项目的所有其他Activity都打开一遍再关闭掉

②现在我们处在首页MainActivity中。此时需要点一下「Force garbage collection」启动一遍GC

③导出hprof文件

④使用这招正则表达式,看看除了MainActivity外还有哪些Activity还没被回收。

⑤逐个对它们执行Path to GC Roots,找出是源头到底哪块关联了强引用,对代码进行调整。

 

3.Object Query Language

OQL类似于SQL,O代表Object对象的意思嘛,S代表.....(妈的不谷歌一下还真忘了S代表什么意思)

输入SELECT * FROM "com.yao.memorytest.*Activity"搜索出来的结果比正则给力,不会把内部类也查出来了。

或者输入SELECT * FROM INSTANCEOF android.app.Activity 把java语法里面instanceof Activity的对象搜索出来。

更多OQL语法可以谷歌。

以上3方法得到表格后,只能根据自身App运行情况和业务逻辑分析,加上查看Retained Heap大小的排序逐个排查了。

 

 

还有一个给力的内存泄漏检测工具叫LeakCanary,可以参考链接LeakCanary 中文使用说明

 

 

常见的内存泄漏,个人总结起来只有3类

1.跟静态(static)相关的。

比如说单例中使用到Context,传递了Activity这个Context而不是ApplicationContext。比如有些博主举例用 static 修饰一个View对象。比如static修饰了一个集合类,这集合类有很多元素或者里面元素都很大。

2.任务型工作。

比如Handler导致的内存泄漏,是因为 postDelayed 或者 sendMessageDelayed 发送了延时任务,详情看我之前文章。还有 AsyncTask 任务或者其他耗时网络请求。应该在离开(一般退出Activity)的时候取消掉。

3.一些资源打开或注册后,需要及时的关闭或注销。

比如File,数据库,IO,还有广播,eventbus,一些Listener等这些需要有开有关这样的成对编程。

 

 

 

参考

http://wiki.eclipse.org/MemoryAnalyzer

 

 

展开阅读全文

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