JVM 对象存活性判断

GC 从其底层实现方式(即 GC 算法)来看,大体可以分为两大类:基于可达性分析的 GC 和基于引用计数法的 GC。当然,这样的分类也不是绝对的,很多现代 GC 的设计就融合了引用计数和可达性分析两种。
  • 可达性分析法:基本思路就是通过根集合(GC Root)作为起始点,从这些节点出发,根据引用关系开始搜索,所经过的路径称为引用链,当一个对象没有被任何引用链访问到时,则证明此对象是不活跃的,可以被回收。使用此类算法的有 JVM、.NET、Golang 等。
  • 引用计数法:引用计数法没有用到根集概念。其基本原理是:在堆内存中分配对象时,会为对象分配一段额外的空间,这个空间用于维护一个计数器,如果有一个新的引用指向这个对象,则计数器的值加 1;如果指向该对象的引用被置空或指向其它对象,则计数器的值减 1。每次有一个新的引用指向这个对象时,计数器加 1;反之,如果指向该对象的引用被置空或指向其它对象,则计数器减 1;当计数器的值为 0 时,则自动删除这个对象。使用此类算法的有 Python、Objective-C、Perl 等。

基于引用计数法的 GC,天然带有增量特性(incremental),GC 可与应用交替运行,不需要暂停应用;同时,在引用计数法中,每个对象始终都知道自己的被引用数,当计数器为 0 时,对象可以马上回收,而在可达性分析类 GC 中,即使对象变成了垃圾,程序也无法立刻感知,直到 GC 执行前,始终都会有一部分内存空间被垃圾占用。

上述两类 GC 各有千秋,真正的工业级实现一般是这两类算法的组合,但是总体来说,基于可达性分析的 GC 还是占据了主流,究其原因,首先,引用计数算法无法解决循环引用无法回收的问题,即两个对象互相引用,所以各对象的计数器的值都是 1,即使这些对象都成了垃圾(无外部引用),GC 也无法将它们回收。当然上面这一点还不是引用计数法最大的弊端,引用计数算法最大的问题在于:计数器值的增减处理非常繁重,譬如对根对象的引用,此外,多个线程之间共享对象时需要对计数器进行原子递增/递减,这本身又带来了一系列新的复杂性和问题,计数器对应用程序的整体运行速度的影响。

引用计数(Reference Counting)

引用计数器在微软的 COM 组件技术中、Adobe 的 ActionScript3 种都有使用。引用计数器的原理很简单,对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1,当引用失效时,引用计数器就减 1。只要对象 A 的引用计数器的值为 0,则对象 A 就不可能再被使用。引用计数器的实现也非常简单,只需要为每个对象配置一个整形的计数器即可。

GC 引用计数示意图

但是引用计数器有一个严重的问题,即无法处理循环引用的情况。因此,在 Java 的垃圾回收器中没有使用这种算法。一个简单的循环引用问题描述如下:有对象 A 和对象 B,对象 A 中含有对象 B 的引用,对象 B 中含有对象 A 的引用。此时,对象 A 和对象 B 的引用计数器都不为 0。但是在系统中却不存在任何第 3 个对象引用了 A 或 B。也就是说,A 和 B 是应该被回收的垃圾对象,但由于垃圾对象间相互引用,从而使垃圾回收器无法识别,引起内存泄漏。

引用树遍历

所谓的引用树本质上是有根的图结构,它沿着对象的根句柄向下查找到活着的节点,并标记下来;其余没有被标记的节点就是死掉的节点,这些对象就是可以被回收的,或者说活着的节点就是可以被拷贝走的,具体要看所在 HeapSize 中 的区域以及算法,它的大致示意图如下图所示(注意这里是指针是单向的):

GC Root Set

首先,所有回收器都会通过一个标记过程来对存活对象进行统计。JVM 中用到的所有现代 GC 算法在回收前都会先找出所有仍存活的对象。下图中所展示的 JVM 中的内存布局可以用来很好地阐释这一概念:

GC 不可达对象

而所谓的 GC 根对象包括:当前执行方法中的所有本地变量及入参、活跃线程、已加载类中的静态变量、JNI 引用。接下来,垃圾回收器会对内存中的整个对象图进行遍历,它先从 GC 根对象开始,然后是根对象引用的其它对象,比如实例变量。回收器将访问到的所有对象都标记为存活。存活对象在上图中被标记为蓝色。当标记阶段完成了之后,所有的存活对象都已经被标记完了。其它的那些(上图中灰色的那些)也就是 GC 根对象不可达的对象,也就是说你的应用不会再用到它们了。这些就是垃圾对象,回收器将会在接下来的阶段中清除它们。

不过那些发现不能到达 GC Roots 的对象并不会立即回收,在真正回收之前,对象至少要被标记两次。当第一次被发现不可达时,该对象会被标记一次,同时调用此对象的 finalize()方法(如果有);在第二次被发现不可达后,对象被回收。利用 finalisze() 方法,对象可以逃离一次被回收的命运,但是只有一次。逃命方法如下,需要在 finalize() 方法中给自己加一个 GCRoots 中的 hook:

public class EscapeFromGC(){
   public static EscapeFromGC hook;
   @Override
   protected void finalize() throws Throwable {
      super.finalize();
      System.out.println("finalize mehtod executed!");
      EscapeFromGC.hook = this;
}
下一节:在 JDK 1.2 以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及(reachable)状态,程序才能使用它。从 JDK 1.2 版本开始,把对象的引用分为 4 种级别,从而使程序能更加灵活地控制对象的生命周期。这 4 种级别由高到低依次为:强引用、软引用、弱引用和虚引用。