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) //纯粹为了占用空间
}
在以上情况中,若用引用计数法,虽然gcTest1
与gcTest2
赋空后不可能再被使用,应被收集,但由于gcTest1
与gcTest2
的相互引用关系未取消,对象的引用计数器不为0,因此不会被收集。在JVM
下运行以上代码,并通过追加命令行参数来打印GC
日志:
scalac GCTest.scala
scala -XX:+PrintGCDetails GCTest
打印日志中具有以下信息:
说明对象虽有相互引用,但仍被处理收集了。
若注释掉System.gc()
一行,即不触发GC
,同样查看GC
日志,信息如下:
这说明这两个对象确确实实是在我们触发垃圾收集时被清空的,也进一步印证了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
输出如下:
可见,第一次循环时,虽然对象状态为不可达,但由于成功触发了Finalize()
方法,并在Finalize
方法中重新建立了引用链,因此对象在第一次循环中的垃圾收集中存活了下来,成功完成自救,但由于Finalize()
只能被触发执行一次,因此在之后的循环中,Finalize
将不会被执行,对象无法自救,只有被回收。