发布于 

从JVM垃圾回收机制看Android内存泄露

JVM垃圾回收机制

JVM内存模型

了解JVM垃圾回收机制之前,有必要先介绍JVM的内存模型机制,以阐明我们常说的JVM垃圾回收机制是从何处回收垃圾。

注:这里提到的JVM内存模型和java内存模型并不是一回事,注意区分

在Java中,我们的源代码编译后会生成以.class为后缀的字节码(在Android中会通过dx工具进一步翻译、重构、解释、压缩成Android虚拟机支持的Dex文件)

Class字节码经过类加载系统的处理,会把执行程序需要的相关信息存储到内存中的如下区域:

PC寄存器:线程私有,存储每个线程下一步将执行的JVM指令

JVM栈:线程私有,存放局部变量,操作数栈,返回结果等。非基本类型的对象,在JVM栈中只存储对象的引用(即一个指向堆上的地址),而对象数据存储在堆上。

本地方法栈:线程私有,native方法的调用栈

方法区:线程共享,其中包含一个运行时常量池。存储已被虚拟机加载的类信息、常量、静态变量、即时编译器(jit)编译后的代码数据。

堆:线程共享,用来存储对象实例的区域。

堆区分为新生代(包含Eden space和survivor space)、老年代。

新生代:

  • Eden区:对象被创建的时候首先放在Eden区,如果Eden区满就会触发一次Minor GC(定义见下文),之后没有被回收的对象会从Eden区转移到Survivor区。
  • Survivor区:Survivor区分为To Survivor、 From Survivor,触发GC时,不能被回收的对象被放入到To Survivor,同时Eden区域的内存会在垃圾回收的过程中全部释放,之后互换To Survivor和From Suvivor的标签,保证To Suvivor区始终为空。

在Minor GC中存活下来的对象age会加1,当age超过一定阈值(即在多次GC中存活下来,已足够old)的对象会被转移到老年代。

老年代:用于存放新生代中多次存活足够old的对象、也有可能是新生代分配不了内存的大对象,当老年代被放满的之后,虚拟机会进行垃圾回收,称之为Major GC。由于Major GC需对整个堆进行扫描和回收,因此又称为Full GC。此外,在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果大于则进行Minor GC,如果小于则看HandlePromotionFailure设置是否允许担保失败(不允许则直接Full GC)。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试Minor GC(如果尝试失败也会触发Full GC),如果小于则进行Full GC。

堆区大小=新生代大小+老年代大小,其中新生代:老年代为1:2,Eden:From Survivor:To Survivor为8:1:1。

垃圾回收是回收程序执行过程中不需要的对象相关的内存,因此主要发生在堆区(对象实例)和JVM栈区(对象引用),少部分发生在方法区。

垃圾的判定:对象存活算法

了解完从何处回收垃圾之后,再来讲讲什么样的对象会被判定为回收目标,常见的对象存活算法有:

引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器+1,当引用失效,计数器-1.任何时刻计数器为0的对象就是不可能再被使用的。

  • 优点:实现简单,判定效率高效,被actionscript3和python中广泛应用。
  • 缺点:无法解决对象之间的相互引用问题。java没有采纳

可达性分析算法

通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GCRoots没有任何引用链相连的时候,则证明此对象是不可用的。

比如如下,右侧的对象是到GCRoot时不可达的,可以判定为可回收对象。

  • 虚拟机栈中引用的对象。
  • 方法区中静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法中JNI引用的对象。

对象可达性分析之后,发现没有与GCRoots相连接,此时会被第一次标记并筛选

Java的四种引用类型

1.强引用:程序中普遍存在的一种引用 Object o = new Object();垃圾收集器永远不会回收被强引用的对象。

2.软引用:如下所示,当我们存在有用但不是必需的对象时,例如缓存,就可以使用软引用。

只要内存空间足够,软引用对象就不会被回收,将要发生内存溢出异常时,会将软引用的对象回收。

1
SoftReference<String> sr = new SoftReference<String>(new String("hello"));

3.弱引用:弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。

1
WeakReference<String> sr = new WeakReference<String>(new String("hello"));

4.虚引用(了解必要性低):虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

垃圾回收过程

垃圾回收算法的思路

了解存活性算法和Java引用类型的基础上,再来讨论一下垃圾回收算法的几种思路

  • 标记-清除算法:先标记出需要被回收的对象,然后全部清除,这种算法有两个严重的问题, 一是标记清楚的效率不高,二是产生内存碎片
  • 标记-复制算法 :将内存划分为相等的两块区域A和B,一次只用其中一块A,当需要垃圾回收时,将A中所有存活的对象复制到B,然后清楚A,使用B。就这样周而复始。但是也有一个明显的问题:可使用的内存大小只有一半。
  • 标记-整理算法 :先标记,然后让所有存活对象向另一端移动,然后直接清理端边界以外的内存,这样可以解决内存碎片问题

分代收集算法

Java中真正采用的分代收集算法是标记复制算法和标记整理算法的结合:

新生代:

所有新对象创建发生在Eden区,Eden区满后触发新生代上的minor GC,将Eden区和非空闲Survivor区存活对象复制到另一个空闲的Survivor区中。

永远保证一个Survivor是空的,新生代minor GC就是在两个Survivor区之间相互复制存活对象,直到Survivor区满为止。

由于新生代大多数对象是 “朝生夕死” 的所以,对于新生代采用有两个很小的Survivor区、一个大的Eden区,使用复制算法的原理进行回收:一次使用一个Survivor区1和Eden区,生还的对象移入另一个保留区2,然后清空所有,周而复始。

老年代

Eden区满后触发minor GC将存活对象复制到Survivor区,Survivor区满后触发minor GC会有部分足够old的存活对象复制到老年代。

经过新生代的两个Survivor之间多次复制,仍然存活下来的对象就是年龄相对比较老的,就可以放入到老年代了,随着时间推移,如果老年代也满了,将触发Full GC,针对整个堆(包括新生代、老年代)进行垃圾回收。

老年代大多数对象会长期存活,不适合复制算法,所以使用标记-整理算法。

Android内存优化思路:如何避免内存泄露及OOM

  • 不再使用对象时,将引用置空,那么GC顺着栈或者静态存储区中的引用就找不到堆中对应的对象,那么这个对应的对象就会被回收
    • Android中Activity,context,bitmap等占据较大的内存空间对象,在不用的时候,一定保证当前对象的直接引用和间接引用全部被置为空内存才能被释放。
  • 尽量不要在生命周期较长(程序执行期间可能都不会被收回)的对象实例中引用其他的不必要的占内存较大的对象实例
  • 内存优化一个最根本的准则, 就是努力使你的程序适配Java的GC机制

参考

JVM内存优化-图解垃圾回收

《深入理解JAVA虚拟机》-周志明


本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

本站由 @tsparrot 创建,使用 Stellar 作为主题。