java内存管理与c/c++不同,java使用garbage collection
机制,由虚拟机管理内存。在大部分虚拟机(包括android的ART)中,都采用了“可达性”分析算法来进行内存管理。
原理是:选取某几个root节点,从root开始层层遍历,如果找不到对该对象的引用链,则该对象被标记为不可达,等待gc回收。
如果引用链中长期存在着对该对象的引用(强引用),则该对象一直不能被gc销毁。一两次并没有多大影响,如果频繁发生,则可用内存会逐渐不足,在某次申请内存时会发生OOM,导致程序崩溃。
这里强调是强引用,下面简单介绍下java中的几种引用类型:
ReferenceQueue
的轨迹,允许获知对象何时从内存中清除。所以如果需要使用在可能发生内存泄漏的环境中引用Activity
、Bitmap
等,使用WeakReference
最好。
由上文可知,泄露是由于长期持有对象的强引用导致,所以我们可以用以下方法强行制造泄露。
用static
修饰的变量或者引用,它们的所属的是类,生命周期与类的生命周期一致,贯穿App的启动到关闭。所以如果用static
修饰一个大对象的引用,就会产生泄露。例如:
static Activity activity;
static View view;
Activity是四大组件之一,其对象占有的内存较大,用static
修饰容易发生泄露。
View虽然占有的内存并不大,但是其构造函数中需要传入Context
引用,一般我们传入的是Activity
,也就间接持有了Activity
的引用,容易泄露。
非静态内部类会持有外部类的引用。具体见这篇文章。大致上就是说在编译阶段,编译器会给非静态加上一个成员变量,其类型与外部类类型相同,在构造方法的参数上添加上外部类的一个引用变量,并在初始化内部类的时候传入外部类的实例。如下:
//以下并不符合java代码规范,仅演示在编译阶段添加的代码
public class Test(){
public static void mian(String[] args){
new InnerTest(this); // 将当前实例引用传给内部类构造函数的第二个参数
}
class InnerTest{
Test test; // 添加一个引用变量
InnerTest(InnerTest this, Test test){ // 添加一个当前类的this变量和外部类的引用变量
this.test = test; // 对引用变量赋值
}
}
}
一旦非静态内部类实例长期存活,由于强引用的关系,外部类实例也长期存活,这样就造成了可能的内存泄漏。
例如AsyncTask
处理后台任务,我们很多时候采用匿名内部类去创建:
void leakAsyncTask(){
new AsyncTask<Void, Void, Void>(){
@Override
protected Void doInBackground(Void... voids) {
while (true){
....
}
return null;
}
}
}
在AS中写出上述代码,AS会提醒我们需要使用静态内部类否则会出现内存泄漏,无独有偶,当我们使用Thread
和Handler
的时候同样会提醒我们去创建静态内部类。
当我们使用第三方库的时候,比如ButterKnife等等,有时候需要获取系统服务,我们在创建对象的时候会将Activity
实例的引用传过去进行register
或者bind
,而它们在运行的时候我们不易察觉,如果我们没有在Activity
被销毁之前取消订阅或者绑定,由这些后台程序持有着我们Activity
的引用,就会造成内存泄漏。
AS中的Android Profiler是专门用来检测应用的运行情况:
左上角的垃圾箱形状的按钮是用来强制进行一次gc,第二个按钮是Dump java heap
,这个按钮可以产生一个当前java堆的.hprof文件,用来记录当前时刻java堆中的内存情况。
而图中各种颜色的含义都在图上标明了,可以看到蓝色的变化很大,并且过段时间就会骤降,很显然这代表了java的堆内存,可以很清晰地看到各个时刻的内存情况。
下面看看真实的内存泄露是什么样子,我在一个Activity
中启动了另外一个Activity
,在后者中增加一个static
变量mContext
,并将该Activity
实例引用赋值给该变量,显然按照上面的说法是会出现泄露的,我们看看结果:
图中的上升沿是我点击启动另一个Activity
时的内存变化,由于static
变量一直持有着Activity
的引用,所以gc时无法销毁,可以看到与上面图的区别就是只有上升沿没有下降沿。长时间肯定会出现内存崩溃。
在上面频繁启动Activity
后点击Dump java heap
按钮,过一会我们可以看到AS为我们Dump出此时的内存记录。
找到所在包刚刚定义的类,如下:
可以明显看到本来应该大小相似的两个类,由于持有了static
类型的变量,其占有的内存大小是第一个Activity
的好几倍,如果频繁这样点击,最终肯定会造成内存泄漏:
还可以导出Dump的文件然后用MAT进行分析,但是最简便的方法当然是:
使用Square公司的Leakcanary进行分析,此处是地址。