1.什么是GC?
在分析内存泄露之前首先要了解一下GC,GC(Garbage Collection)就是Java中常提到的垃圾回收,指的是JVM会自动回收不在被引用的内存数据。
2.什么是GC Roots?
GC Roots即Java虚拟机当前存活的对象集,其中的每一个对象可以作为一个GC Root。
在确定一个对象是否需要被回收时,常常会用到可达性分析算法,即通过判断对象的引用链是否可达来决定对象是否可以被回收,如下图:
上图中,从GC Root到ObjectC、ObjectE、ObjectF都不可达,所以这三个对象就是会被GC回收的对象。
3.什么是内存泄露?
内存泄露就是指某些已经没有被使用的对象还存在内存之中,并且GC无法回收它们。如果任由它们常驻内存,程序的运行性能就会受到影响。
造成内存泄露的根本原因就是因为对象在没有使用的时候仍存在一条到GC Root的可达路径,导致GC无法正常回收他们,如下图:
已经使用不到的ObjectC任然间接与GC Root相连,这就会导致GC无法回收它,从而导致内存泄露。
4.如何检测并定位内存泄露?
这里主要介绍两种定位内存泄露的方法:
4.1 使用Androd Studio的Android Monitor里面的Memory
如下图:
先来看看正常情况下是怎么显示的:
在MainActivity里面添加一个按钮,并给他绑定如下的点击事件:
public class MainActivity extends AppCompatActivity {
Button mButton;
List<TextView> List = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.btn_add);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
for (int i = 0 ; i<= 20000;i++){
TextView textView = new TextView(MainActivity.this);
List.add(textView);
}
}
});
}
}
如果这时候点击了Button,内存会这样变化:
如果这时候按下返回键,退出MainActivity,然后手动调用GC(点一下小卡车标志),内存会这样变化:
可以明显看到GC后的内存又降了下来,说明无用的TextView和ArrayList对象被GC正常回收了。
下面构建一个会出现内存泄露场景:一个静态的Context对象持有了当前Activity的引用。(当然在实际开发中多半不会遇到)
public class MainActivity extends AppCompatActivity {
static Context mContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.btn_add);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
for (int i = 0 ; i<= 20000;i++){
TextView textView = new TextView(MainActivity.this);
List.add(textView);
}
}
});
mContext = this;
}
}
执行和刚刚相同的操作,先点Button,再点返回键退出Activity,然后手动GC,发现内存会居高不下:
这时候点击如图所示的Dump Java Heap按钮,等待Dump完成,自动调转到分析界面:
先点击绿色小三角,然后点开Leak Activitys,即可看存在内存泄露的Activity和内存泄露的位置。
根据图中的信息可以清楚地看到是mContext出现了内存泄露,然后再回到代码中找到mContext,即可着手解决这个问题。
4.2使用MAT
MAT(Memory Analyzer)是一款强大的Java堆内存分析工具,可以快速分析内存情况,是由eclipse公司推出的。 它并不会直接告诉我们内存泄露的具体位置,只会给出一些内存和引用信息,我们需要根据这些信息来排查可能出现内存泄露的地方,适合比较复杂的内存泄露的情景。
4.2.1获取hprof文件
它也需要借助hprof文件来分析内存,所以首先得要获取hprof文件,获取它的方式有两种,一种就是刚刚使用的Android Monitor中的Dump Java Heap,但是获取到的hprof文件需要稍作转换才能供MAT使用,如图:
还有一种方式就是通过DDMS来获取hprof,如图:
打开DDMS先选择heap标签,然后在左边的方框里选择当前的应用的包名,再点一下上方updata heap(绿色小圆柱),之后点击操作一下App(点一下button,然后按下返回键),然后点击右边的Cause GC按钮,最后点击Dump HPROF file(带红色箭头的绿色小圆柱),即可导出hprof文件。
同样,这种方式获取到的hprof文件同样需要转换一下,这里要采用hprof-conv
命令转换,如图:
格式为:hprof-conv
+ 输入文件路径 + 输出文件路径及名称
4.2.2使用MAT排查内存泄露位置
在排查之前,先了解几个名词:
Dominator Tree
:支配树,列出每个对象、大小,常用于分析对象之间的引用结构。(站在实例的角度)
Histogram
:直方图,列出内存中每个对象的名字,大小和数量。(站在类的角度)
Shallow heap
:对象本身占用内存的大小。
Retained Heap
:这个对象以及它所持有的其它引用(包括直接和间接)所占的总内存。
outgoing references
:被当前对象引用的对象。通过它可以看出当前当前”抓着”哪些对象不放。
incoming references
:引用到该对象的对象。通过它可以看出谁在内存中”抓着”当前实例不放。
再来看一个常见的内存泄露的场景:
public class MainActivity extends AppCompatActivity {
static LeakClazz mLeakClazz;
List<TextView> List = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//这个for循环只是为了加大内存占用,让内存显示得更明显
for (int i = 0 ; i<= 10000;i++){
TextView textView = new TextView(MainActivity.this);
List.add(textView);
}
if (mLeakClazz == null){
mLeakClazz = new LeakClazz();
}
}
class LeakClazz{
}
}
众所周知,一个非静态的内部类会持有外部类的引用,然而在这里却创建了一个静态的实例mLeakClazz
,那只要app进程还存在,mLeakClazz
就存在,所以外部类MainActivity就会被一直引用,及时他已经不在使用了。这就造成了内存泄露。在获取到hprof文件后,我们来分析一下内存占用情况。
首先打开获取到的leak.hprof
文件:
然后点开Dominator Tree查看对象之间引用结构:
我们可以使用正则搜索功能,直接搜索与MainActivity有关的引用信息:
找出与GC Roots相连的路径,由于软引用、弱引用和虚引用并不会影响GC的运行,所以可以直接过滤掉他们:
这样就可以找到内存泄露的位置了:
至此,一个就可以去代码中修改内存泄露的地方。整个过程看似简单,这里这时简单的演示了一下,但在实际开发中需要靠经验和工具的结合方可定位内存泄露的地方,下面就总结一下常用内存泄露的场景:
1.创建了非静态的内部类的静态实例
上面的例子已经分析过了。
2.单例模式持有外部对象的引用
单例对象会在JVM中以静态变量的形式一直存在,如果持有了外部对象的引用,那么外部对象将无法被GC回收,特别是在单例模式中需要用到context的时候,如果能够使用Application的context就尽量使用,如果不能,就要考虑能不能使用软引用或弱引用了。下面是一个单例模式可能造成内存泄露的场景:
public class Singleton {
public Context mContext;
private Singleton(){}
private static class Holder{
private static Singleton INSTANCE = new Singleton();
}
public Singleton getINSTANCE(){
return Holder.INSTANCE;
}
public void doSomething(Context context){
mContext = context;
}
}
如果调用doSomething(Context context)方法的时候传入了Activity的Context就必然会造成内存泄露。
3.使用线程进行耗时操作的时候没有与Activity的生命周期保持一致
线程使用的是内部类或者Runnable实现的时候由于持有外部类(Activity)的引用,若是在Activity退出的时候线程还没执行完,它将一直持有Activity的引用,这就会造成内存泄露。就像这样:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//这个for循环只是为了加大内存占用,让内存显示得更明显
for (int i = 0 ; i<= 10000;i++){
TextView textView = new TextView(MainActivity.this);
List.add(textView);
}
//匿名内部类会持有外部类的引用
new Thread(new Runnable() {
@Override
public void run() {
//模拟一个耗时操作
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
解决的方法就是在activity被finish()的时候结束线程:
public class MainActivity extends AppCompatActivity {
Thread t;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
for (int i = 0 ; i<= 10000;i++){
TextView textView = new TextView(MainActivity.this);
List.add(textView);
}
t = new Thread(new Runnable() {
@Override
public void run() {
Log.d("test", "run: ");
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
t.interrupt();
}
}
4.使用一些系统资源后未及时关闭
使用了BraodcastReceiver,File, Cursor,Stream,Bitmap等资源后一定要及时关闭,不然也会造成内存泄露。
总而言之,熟悉一些常见的内存泄露场景,再了解一下该如何借助工具去分析内存泄露,就可以在实际开发中大大避免这一问题。
此外, leakcanary在一定程度上还更加容易帮助我们检测内存泄露,也是一款神器。