深入理解Android内存泄漏

JAVA内存

JAVA的JVM的内存可分为3个区:堆(heap)、栈(stack)和方法区(method)

堆区:

  1. 存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
  2. jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身

栈区:

  1. 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
  2. 每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
  3. 栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)

方法区:

  1. 又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量
  2. 方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public   class  AppMain                //运行时, jvmappmain的信息都放入方法区    
{
public static void main(String[] args) //main 方法本身放入方法区。
{
Sample test1 = new Sample( " 测试1 " ); //test1是引用,所以放到栈区里, Sample是自定义对象应该放到堆里面
Sample test2 = new Sample( " 测试2 " );

test1.printName();
test2.printName();
}
}

public class Sample //运行时, jvmappmain的信息都放入方法区
{
/** 范例名称 */
private name; //new Sample实例后, name 引用放入栈区里, name 对象放入堆里

/** 构造方法 */
public Sample(String name)
{
this .name = name;
}

/** 输出 */
public void printName() //print方法本身放入 方法区里。
{
System.out.println(name);
}
}

GC(垃圾回收器)

首先我们需要知道,GC它会回收哪部分的内存,

回收什么

在Java的运行时数据区中,程序计数器、虚拟机栈、本地方法栈三个区域都是线程私有的,随线程而生,随线程而灭,在方法结束或线程结束时,内存自然就跟着回收了,不需要过多考虑回收的问题。而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾回收器关注的是这部分内存

什么时候回收

Minor GC ,Full GC 触发条件或者手动GC

如何回收

现阶段主流的JVM采用的都是可达性分析算法

在主流商用程序语言的实现中,都是通过可达性分析(tracing GC)来判定对象是否存活的。此算法的基本思路是:通过一系列的称为“GC Roots”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是GC Roots 到这个对象不可达)时,则证明此对象时不可用的

GC为了能够正确释放对象,会监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用

Android中采用了标注与清理(Mark and Sweep)回收算法

从”GC Roots”集合开始,将内存整个遍历一次,保留所有可以被GC Roots直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收

GC Roots

那什么可以是 GC Roots?

  • 虚拟机栈(栈帧中的局部变量表,Local Variable Table)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

GC流程

看图

每个圆形节点代表一个对象(内存资源),箭头表示对象引用的路径(可达路径),黄色表示遍历后的当前对象与GC Roots存在可达路径

然后进行遍历

当圆形节点与GC Roots存在可达路径的时候,表示当前对象正在被使用,GC不会将其回收。反之,若圆形节点与GC Roots不存在可达路径,意味着这个对象不再被程序引用(蓝色节点,虽然蓝色部分可能存在互相引用,还是会被回收),GC可以将之回收

在Android中,每一个应用程序对应有一个单独的Dalvik虚拟机实例,而每一个Dalvik虚拟机的大小是固定的(如32M,可以通过ActivityManager.getMemoryClass()获得)。这意味着我们可以使用的内存不是无节制的。所以即使有着GC帮助我们回收无用内存,还是需要在开发过程中注意对内存的引用。否则,就会导致内存泄露。

内存泄漏

综上所述,我们可以知道,什么是内存泄漏

内存泄漏
内存泄漏指:我们不再需要的对象资源仍然与GC Roots存在可达路径,导致该资源无法被GC回收。

什么意思呢?就是一个不需要的对象本应该被GC回收掉的,但是GC Roots存在可达路径,因此它无法回收这个我们不需要的对象,然后这个对象我们又不再用它了,它就占用了内存空间,占用了无用的内存空间,因此,它叫内存泄漏

内存泄漏的概念系统它是不管的,它不知道谁泄漏谁不泄漏,它只会在内存不够用了给你OOM,因此我们把那些会造成无用内存空间的操作称之为内存泄漏,这样开发者可以去避免代码OOM的危险

Android中的内存泄漏

拿一个非常经典的案例,也就是单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Person{
Context context;
static Person person;
Person(Context context){
this.context = context;
}
static Person getPerson(Context context){
if(person==null){
return new Person(context);
}else{
return person;
}
}
}

在Person这个类中

  • person对象满足 gc roots 的要求
  • 它有自己的 gc roots 树
  • 树下会有 context
  • 因此,这个context它是可达的,
  • 并且这个单例的存活时间与App程序的生命周期一致
  • 既然是可达的,gc的时候就不会去回收它,那么它就会一直存在
  • 从逻辑上而言,我们Activity结束后,这个context应该是要被回收的,实际上并没有
  • 如果频繁去使用这个方法,频繁去结束Activity,Person类中就会有越来越多的占用无用内存的context
  • 因此,它会造成内存泄漏

解决方案也很简单,使用applicationContext或者弱引用

再举一个Handler的例子,假设写了一个Handler,非静态,在handler中进行 postDelayed 的操作

非静态内部类会持有外部类的引用,也就是我们的Activity

对于内部类,系统会在它不需要工作的时候去回收

在 postDelayed 的时候我们去关闭Activity

在 postDelayed时

  • Handler会把消息放到Looper的MessageQueue中
  • 而且Message是持有Handler的
  • 而Handler又持有Activity
  • 也就是Looper和Activity之间存在调用链
  • 在MessageQueue的 gc roots 下,handler是可达的,因此不能被回收
  • 因而发生泄漏

参考链接:Google I/O: Memory Management for Android Apps