MAT检测模拟内存泄露
名词解释:
MAT(Memory Analyze Tool):顾名思义,MAT就是内存分析工具。是一个快速且功能丰富的堆转储分析软件,它可以帮助你找到内存泄露和分析高内存消耗问题。
内存溢出(OOM):即Out of Memory。内存溢出是指APP向系统申请超过最大阀值的内存请求,系统不会再分配多余的空间,就会造成OOM error。
内存泄露(memory leak):当一个对象已经不需要再使用了,本该被回收时,而另外一个正在使用的对象持有它的引用从而导致它不能被回收,这就导致本该被回收的对象不能被回收而停留在堆内存中。
GC(Garbage Collections): 字面意思是垃圾回收器,释放垃圾占用的空间。
首先我们先来了解为什么使用这个工具:
Android系统为每个应用程序分配的内存有限,当一个应用中产生的内存泄漏(Memory Leak)比较多时,就难免会导致应用所需要的内存超过这个系统分配的内存限额,这就造成了内存溢出(Out of Memory)而导致应用Crash。
事前准备:
安装MAT:
MAT是Eclipse的一个插件,我们使用Android Studio配合MAT定位内存泄露问题之前,要先下载MAT工具。
下载地址:https://www.eclipse.org/mat/downloads.php
在ubuntu系统,下载解压后即可使用
安装hprov-conv:
Android Studio中的Android Profiler生成的hprof文件不能够被MAT直接打开,需要通过以下命令进行转换
hprof-conv -z <源文件> <产出文件>
所以需要提前安装好,安装命令如下:
$sudo apt-get install hprof-conv
模拟内存泄露:
ActivityA作为程序的入口,通过ActivityA打开ActivityB,ActivityB在传入自身的context获取AppManager单例,然后点击Button创建多个ImageView对象并保存到List中;然后退出ActivityB,在profiler手动GC,此时AppManager持有对ActivityB的引用,导致ActivityB不能够被正常回收。
ActivityA只有一个按钮跳转ActivityB,这里就不把ActivityA的代码贴出来了。
ActivityB代码如下:
public class ActivityB extends Activity {
private List list;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_b);
AppManager.getInstance(this);
list = new ArrayList();
Button button = findViewById(R.id.ActivityB_button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
for (int i = 0; i < 10000; i++) {
ImageView imageView = new ImageView(ActivityB.this);
list.add(imageView);
}
}
});
}
}
AppManager的代码如下
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
}
public static AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}
}
通过Android Studio中的Profiler(关于Profiler的操作可以自行百度)获取hprof文件:
打开Profiler,选择分析我们想要的进程,然后打开Memory分析界面。下面开始模拟内存泄露过程
1.通过ActivityA打开ActivityB,随后多次点击ActivityB中的Button,可以看到内存的使用快速地增长。
2.按强制gc,内存没变。是因为所添加的imageView都被list所引用着
3.此时,我们退出ActivityB,再次多次触发Froce GC,我们理想的效果是回收内存,内存使用状况与打开ActivityB之前接近。不过实际情况是内存的占用依然没下去,此时就产生了严重的内存泄露(Momery Leak)。
4.点击Dump Java heap,然后Export生成的Heap Dump到计算机中(建议新建一个文件夹,MAT在打开hprof文件时,会产生很多文件)。
5.最后在终端输入下面的命令:
hprof-conv -z memory-20190531T154103.hprof memory-20190531T154103_mat.hprof
//hprof-conv -z <源文件> <产出文件>
获取到能被MAT工具所打开的hprof文件。
MAT工具的使用:
打开MAT,导入hprof文件。
成功导入后会有一个入门向导,让我们选择打开哪些报告,我们后续都可以在面板中打开,所以点击cancel就好。
Overview窗口:
打开之后的界面如下图;只有一个Overview窗口展示应用的内存的使用情况。
Histogram窗口:
我们点击如下按钮,打开Histogram窗口。Histogram可以展示某个特定类的对象个数和每个对象使用的内存。
界面如下:
我们点击Group by class根据类分组
表格中有4个列名Class Name,Objects,Shallow Heap和Retained Heap。
Class Name:类名
Objects:对应的对象个数
Shallow Heap:一个对象消费的内存数量
Retained Heap:回收所有对象的内存数量
我们可以看到有40000个ImageView的对象,这就是我们刚才内存泄露,无法被正常回收的对象,占用了大部分的内存资源。
如果我们想要看到哪里调用了这个类,右键点击想要查看的类–>Merge Shortest Path to GC Roots–>exclude all phantom/weak/soft etc.referencer
可以清晰地看到引用关系,AppManager类的实例的成员变量context含有对ActivityB对象的引用,导致ActivityB对象中的list无法被释放。
Dominator tree窗口:
我们点击如下按钮,打开Dominator tree窗口。由你系统中的复杂的Object graph(对象引用图)生成的树状图。Dominator tree可以让你分别出最大内存图表。如果所有指向对象Y的路径都经过对象X,则认为对象X支配对象Y。通过查看本例的Dominator tree,我们开始明白到底是哪些内存块发生了泄露。
界面如下:
模拟内存泄露的解决办法:
我们先来分析一下内存泄露的原因:AppManager的一个静态实例持有对ActivityB对象的context的引用,导致退出ActivityB时,ActivityB对象和对象中的成员变量也无法被正常回收。
这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:
1)、传入的是Application的Context:这将没有任何问题,因为单例的生命周期和Application的一样长;
2)、传入的是Activity的Context:当这个Context所对应的Activity退出时,由于该Context和Activity的生命周期一样长(Activity间接继承于Context),所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。
所以正确的单例应该修改为下面这种方式:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context.getApplicationContext();
}
public static AppManager getInstance(Context context) {
if (instance != null) {
instance = new AppManager(context);
}
return instance;
}
}
修改后,我们重复上面获取hprof文件的步骤:
1.通过ActivityA打开ActivityB,随后多次点击ActivityB中的Button,可以看到内存的使用快速地增长。
2.多次触发Force GC,内存的占用一直下不去。因为ActivityB持有List对象的引用,List对象不会被回收。
3.此时,我们退出ActivityB,再次多次触发Froce GC。ActivityB中占用的内存正常被回收。
延伸:
常见的的内存泄露:
1.单例造成的内存泄漏
2.非静态内部类创建静态实例造成的内存泄漏
3.Handler造成的内存泄漏
4…
参考
关于Android中内存泄露与如何有效避免OOM总结:https://blog.csdn.net/gjnm820/article/details/51579080
Memory Analyzer的使用技巧:https://www.jianshu.com/p/759e02c1feee