在Android开发中,内存泄漏是比较常见的,大多数开发者都知道有这么一回事,但是不清楚是什么原因会导致内存泄漏。在此就分享下自己的看法。
在讲内存泄漏之前,我们先了解下Java虚拟机内存模型和GC算法,这样我们能更好的理解后面说的几种情况为什么会导致内存泄漏。
Java虚拟机内存模型
在Java虚拟机规范中指明了Java虚拟机运行时的内存模型,如下图所示
- Java虚拟机栈:虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧( Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 本地方法栈:和Java虚拟机栈类似,只不过是用于Native方法。
- 程序计数器:可以理解成当前线程所执行的字节码行号指示器,用于控制字节码的执行顺序。
- 堆:Java堆是虚拟机内存最大的一块区域,用于存放对象实例,它是被所有线程共享的内存区域。GC管理的主要也是Java堆,也就是说GC回收的内存主要是堆内存
- 方法区:方法区是被各个线程共享的内存区域,该区域用于存放被虚拟机加载的类信息,常量,静态变量等数据
GC算法
我们知道Android4.4及以前采用的是Dalvik虚拟机,Android5.0以后采用的是ART替代Dalvik虚拟机,虽然Dalvik和ART不是Java虚拟机,但是他们的内存模型是相似的。我们以Dalvik虚拟机为例讲解下它的GC算法。
Dalvik
Android的Dalvik虚拟机的GC算法采用的是Mark-Sweep(标记-清除)算法,所谓标记-清除算法,从字面理解可知该算法有两个阶段:标记阶段和清除阶段。标记阶段是把所有活动对象都做上标记的阶段;清除阶段是把那些没有标记的对象,也就是非活动对象回收的阶段。具体的算法实现我们先不关注,主要关注如何区分“活动对象”和“非活动对象”。在Dalvik虚拟机中判断对象是否是活动对象的方式是判断该对象和“GC Roots对象”是否存在引用链。
如上图所示和GC Roots存在存在引用链的Obj1、Obj2、Obj3、Obj4就属于活动对象;Obj5、Obj6、Obj7因为和GC Roots对象不存在引用关系,它们就属于非活动对象,而这些对象占用的堆内存在清除阶段就会被回收。现在就有个关键点,哪些对象可以是“GC Roots对象”呢,GC Roots对象包括以下几种:
- 方法区中的常量引用的对象;
- 方法区中静态变量引用的对象;
- 本地方法栈中JNI引用的对象;
- 虚拟机栈中引用的对象;
内存泄漏
内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放。在Android中常见的内存泄漏场景有:
- 静态变量导致的内存泄漏;
- 匿名内部类和非静态内部类导致的内存泄漏;
- 集合类导致的内存泄漏;
- 资源未关闭导致的内存泄漏;
静态变量导致的内存泄漏
前面我们知道,静态变量引用的对象属于“GC Roots对象”,所以当我们定义静态变量对象的时候就要留意该对象本身和与该对象存在引用关系的对象是否会导致内存泄漏。下面有几个与静态变量相关的例子:
单例
静态变量导致的内存泄漏中有个常见的例子,就是在单例中需要到Context对象的时候,很多人可能会这样写:
public class SingleInstanceLeak {
private static final String TAG = "SingleInstanceLeak";
private Context mContext;
private static SingleInstanceLeak sInstance;
private SingleInstanceLeak (Context context) {
mContext = context;
}
public static SingleInstanceLeak getInstance (Context context) {
if (sInstance == null) {
sInstance = new SingleInstanceLeak(context);
}
return sInstance;
}
public void testLog(String msg) {
Log.d(TAG, "testLog, msg: " + msg);
Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();