本文摘自 Android 工程师进阶 34 讲:第02讲:GC 回收机制与分代回收策略
Java 虚拟机的自动内存管理,将原本需要由开发人员手动回收的内存,交给垃圾回收器来自动回收。
垃圾回收,顾名思义,便是 将已经分配出去的,但却不再使用的内存回收回来,以便能够再次分配。
什么是垃圾?
站在 JVM 的角度讲,垃圾指的是死亡的对象所占据的堆空间。识别定位垃圾,以及如何处理垃圾,是垃圾回收的关键所在。
引用计数法与可达性分析
那么如何确定对象已经死亡?
有一种古老的辨别方法:引用计数法(reference counting)。它的做法是为每个对象添加一个引用计数器,用来统计指向该对象的引用个数。一旦某个对象的引用计数器为 0,则说明该对象已经死亡,便可以被回收了。
引用计数法还有一个重大的漏洞,那便是无法处理循环引用对象。
**
a 引用 b,b 引用 a,除此之外没有其他引用指向 a 或 b,在引用计数法中,无法回收它们。
Java 虚拟机采取的是 可达性分析 法。
这个算法的实质在于将一系列 GC Roots 作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。
GC Root
GC Roots 可以简单理解为 由堆外指向堆内的引用(说法不全面)。包括(但不限于)如下几种:
- Java 虚拟机栈(局部变量表)中的引用的对象(Java 方法栈桢中的局部变量)
- 方法区中 静态引用 指向的对象(已加载类的静态变量)
- Native 方法中 JNI 引用的对象
- 仍处于存活状态中的线程对象
垃圾回收的三种方式
垃圾回收的方式主要分为三种:清除,复制,压缩。
标记-清除算法(Mark and Sweep GC)
优点:原理简单,不需要移动对象
缺点:
- 造成内存碎片
- 分屏效率较低
复制算法(Copying)
优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
缺点:堆空间使用效率低(每次只用一半)
标记-压缩算法(Mark-Compact)
优点:不产生内存碎片,也不需要分成两块区域
缺点:压缩算法性能开销
分代回收策略
大部分 Java 对象存活一小段时间,而存活下来的小部分 Java 对象则会存活很长时间。**(也许和 二八法则 很像?)**
这造就了 JVM 分代回收的思想:对于不同代使用不同的回收算法。
JVM 将堆划分为 新生代 和 老年代 两大部分
其中新生代又被划分为 Eden 区和两个 Survivor 区,它们的比例为 8 :1 :1**
新生代主要的回收算法为复制算法
绝大多数刚刚被创建的对象会存放在 Eden 区:
**
当 Eden 区第一次满的时候,会进行垃圾回收。首先将 Eden 区的垃圾对象回收清除,并将存活的对象复制到 S0,此时 S1 是空的。如图所示:
下一次 Eden 区满时,再执行一次垃圾回收。此次会将 Eden 和 S0 区中所有垃圾对象清除,并将存活对象复制到 S1,此时 S0 变为空。如图所示:
如此反复在 S0 和 S1之间切换几次(默认 15 次)之后,如果还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中。如图所示:
一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或者大数组),并 且新生代的剩余空间不足,则这个 大对象会直接被分配到老年代上。
老年代因为对象的生命周期较长,不需要过多的复制操作,所以 老年代一般采用 标记压缩的回收算法。
注意:对于老年代可能存在这么一种情况,老年代中的对象有时候会引用到新生代对象,**那么这个引用也会被作为 GC Roots。这时如果要执行新生代 GC,则可能需要查询整个老年代上可能存在引用新生代的情况,这显然是低效的。所以,老年代中维护了一个 512 byte 的 Card Table**,所有老年代对象引用新生代对象的信息都记录在这里。每当新生代发生 GC 时,只需要检查这个 card table 即可,大大提高了性能。