Java程序运行过程中,Java堆中遍布着程序运行时创建的对象,在JVM运行垃圾收集时,首先要判断对象是否存活,只有对象已经不再存活时,才会被收集掉,但JVM是如何判断对象是否存活的呢?就算法而言,主要有程序计数法和可达性分析法两种方法,JVM采用的是可达性分析法,下面就两种方法做一个简要分析。

引用计数法

该方法是一个较为简单的判断方法,其主要思想是在创建对象时,给对象添加一个引用计数器,该对象每被引用一次,引用计数器的值就会加1,每当有一次引用失效时,引用计数器的值就会减1,当对象的引用计数器为0时,表明没有任何地方有该对象的引用关系,即可判定该对象已经不再存活,当垃圾收集进程开始时,对象就会被回收。该方法的实现简单,效率较高,Python语言便是应用该种方法进行内存管理。但该种方法也有很明显的缺陷,其无法解决循环引用问题。如下面代码所示情况:

object GCTest {
  def main(args: Array[String]){
    var gcTest1 = new GCTester
    var gcTest2 = new GCTester
    gcTest1.reference = gcTest2  //引用gcTest2对象,其引用计数器加1
    gcTest2.reference = gcTest1 //引用gcTest1对象,其引用计数器加1
    gcTest1 = null  //gcTest1设为空对象,但与gcTest2的引用关系未释放,因此引用计数器的值不会发生变化
    gcTest2 = null  //gcTest2设为空对象,但与gcTest1的引用关系未释放,因此引用计数器的值不会发生变化
    System.gc() //触发垃圾收集,gcTest1与gcTest2对象都为空,已经不可能再被访问到,垃圾收集时应被清空,但由于引用计数器不为0,因此在引用计数器方法下不会被收集
  }
}
class GCTester{
    var reference: Any = null  //用于引用外部对象
    private val placeholder = Array[Byte]((1024*1024).toByte)  //纯粹为了占用空间
}

在以上情况中,若用引用计数法,虽然gcTest1gcTest2赋空后不可能再被使用,应被收集,但由于gcTest1gcTest2的相互引用关系未取消,对象的引用计数器不为0,因此不会被收集。在JVM下运行以上代码,并通过追加命令行参数来打印GC日志:

scalac GCTest.scala
scala -XX:+PrintGCDetails GCTest

打印日志中具有以下信息:
GC1.png
说明对象虽有相互引用,但仍被处理收集了。
若注释掉System.gc()一行,即不触发GC,同样查看GC日志,信息如下:
GC2.png
这说明这两个对象确确实实是在我们触发垃圾收集时被清空的,也进一步印证了Java的垃圾收集算法用的不是引用计数法。

可达性分析法

JVM运用的便是可达性分析法来判断对象是否存活,应用该方法来判断是否存活的还有C#Lisp等。该算法的思想是通过一系列被称为GC Root的对象为起点向下搜索,搜索走过的路程成为引用链,若一个对象到GC Root不存在任何引用链,则判定该对象是不可用的,即应被收集。GC Root对象的选取较为抽象和复杂,可作为GC Root的对象包括:虚拟机栈中引用的对象、方法区中类静态引用的对象、方法区中常量引用的对象、Native方法应用的对象。以上面两个对象互相引用的情况为例,虽然存在相互引用关系,但其不存在任何情况下的到GC Root的引用链,因此要被回收。

对象的自我拯救

Java中,即使一个对象为不可达状态,其也不一定会被回收,对象还有一次自我拯救的机会。在垃圾收集过程中,若发现对象状态为不可达,不会被立即回收,而是会被进行一次标记并判断是否有必要执行对象的Finalize()方法,Finalize()方法是Object类的方法,但Object类没有给予实现,若对象复写实现了该方法,则垃圾收集过程中会暂缓对对象的回收,并将其Finalize()方法丢到一个名为F-Queue队列中,去等待Finalizer线程执行,若对象没有复写并实现该方法或方法已执行过一次,则会被立即回收,因此,若在Finalize方法中,对象重新建立了与引用链上的任一对象的关系,就会在该次垃圾回收中存活下来。但是由于垃圾回收会造成GC停顿,需要时间开销,因此垃圾收集过程不一定会等待Finalize()方法执行完毕,且由于是在队列中等待顺序执行的,并且Finalizer线程的优先级极低,因此Finalize()方法的执行时间是不确定的,因此可能存在垃圾收集已完成,对象已被回收但Finalize方法仍未执行的情况。以以下代码来演示对象的自救过程:

object GCSaveTest {
  var Saver: GCSaver = null
  val Hero = new GCSaver()
  class GCSaver{
    def isAlive(){
      println("I'm still alive...")
    }
    override def finalize(){
      GCSaveTest.Saver = GCSaveTest.Hero  //重新建立引用链,以进行进行自救
      println("I saved myself...")
    }
  }
  def main(args: Array[String]){
    Saver = new GCSaver
    for(i <- 1 to 3){  //循环3次执行相同代码,观察输出情况
      Saver = null  //赋空,此时Saver对象无任何引用链
      System.gc()  //触发GC
      Thread.sleep(300)  //线程暂停300毫秒,以确保Finalize方法有充足的时间被执行
      if(Saver == null){
        println("I'm died...")
      }else{
        Saver.isAlive()
      }
    }
  }
}

运行以上代码:

scalac GCSaveTest.scala
scala GCSaveTest

输出如下:
GCSave.png

可见,第一次循环时,虽然对象状态为不可达,但由于成功触发了Finalize()方法,并在Finalize方法中重新建立了引用链,因此对象在第一次循环中的垃圾收集中存活了下来,成功完成自救,但由于Finalize()只能被触发执行一次,因此在之后的循环中,Finalize将不会被执行,对象无法自救,只有被回收。