在Java虚拟机中,堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是确定这些对象之中那些还“存活”着,哪些已经“死去”(死去就是不可能再被任何途径使用的对象)了。
那么对象销毁过程。也就是finalization机制。
这是对象销毁前的一个回调方法:finalize()
它是Object类中的一个方法,在对象被最终回收之前调用一次(只调用一次)。
Java语言是提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
就是Java是允许对象在销毁之前去调用finalize()方法,去调用一些逻辑。(但是一般建议是不使用的)
当垃圾回收器发现没有引用一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法,但是一个对象的finalize()方法只被调用一次。
finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理工作。
比如:关闭文件、套接字和数据库连接等。
在父类Object类中的finalize()源码:
protected void finalize() throws Throwable {
}
就是空的,具体实现什么,就是要在子类重写,写入需要的操作。
但是在实际操作中,永远都不要显示调用某个对象的finalize()方法,应该交给垃圾回收机制调用。 在里面写代码一定要慎重
主要有以下原因:
在finalize()时可能会导致对象复活。
一个书写糟糕的finalize()方法会严重影响GC的性能。
比如:将finalize()写成一个死循环。
finalize()方法的执行时间是没有保障的,它完全是由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行的机会。
这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者更极端的死循环,将会导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的奔溃。(F-Queue队列具体是什么,看下面)
上面提到自己写的finalize()方法会造成对象的复活,那么在这就主要讨论一些对象的生存还是死亡。
这里所说的对象问题都是在垃圾标记阶段算法 中可达性算法中表现。
因为在堆当中产生一个垃圾就回收一个垃圾的这种做法,显然是不合理的,所以在内存当中,是产生一个垃圾,先进行标记,当内存满的时候,再将标记的对象进行回收操作。
所以在标记阶段,在虚拟机中因为finalize()方法的存在,虚拟机中的对象又分为三种可能的状态
可触及的: 从根节点开始,可以到达这个对象。
可复活的: 对象的所有引用都会被释放,但是对象有可能在finalize()中复活。(就是对象已经确定是垃圾了,但是没有调用finalize()方法)
不可触及的: 对象的finalize()被调用,并且没有复活,那么就会j进入不可触及的状态。不可触及的对象时不可能被复活的,因为finalize()方法是只能调用一次的。
在以上3种状态中,是由于finalize()方法的存在,进行的区分,只有在对象到达不可触及时才可以被回收。
即使在垃圾标记阶段算法中可达性算法中的被判定为不可达的对象,但也不是要“非死不可”的,在这个时候它们暂时还处于“缓刑”阶段,要真正的宣告一个对象死亡,最多是要经历两次标记过程的:
● 如果对象在进行可达性分析后发现没有与GC Roots 相连接的引用链,那它将会被第一次标记。
● 随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。
假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都认为“没有必要执行”。
假如对象重写了finalize()方法,且还未被执行,那么该对象将会被放置在一个名叫 F-Queue的队列之中,并在稍后由一条是虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。(这里所说的“执行”是指虚拟机会触发这个方法开始运行,但并不会承诺一定等它运行结束。)
而对象的“复活”就是这里,finalize()方法就是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()方法中成功拯救自己——那么只要重新与引用链上的任何一个对象建立关键即可。例如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那么在第二次标记时它将会被移出“即将回收”的集合;如果对象在这个时候还是没有逃脱,那么基本上它就真的“死去了”。
(在这个之间就会出现一个问题,这个方法的执行时间,完全是由线程决定的,恰好在开始执行finalize()的时候,在执行“复活”的时候线程结束了,那也就复活失败了)
代码演示:
public class FinalizeDemo {
//定义一个类变量 属于GC Root
public static FinalizeDemo demo = null;
//重写了finalize方法,来自于object类。但是这个方法只会被调用一次
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用了finalize方法");
demo = this; //唤醒、复活对象
// 当前被标记等待回收的对象在finalize()方法中与引用链上的对象连接上
}
public static void main(String[] args) {
try {
demo = new FinalizeDemo(); //给demo赋上了对象 new FinalizeDemo()有了引用
//对象第一次拯救自己
System.out.println("第一次:");
demo = null; //意思就是 new FinalizeDemo();失去引用成为垃圾
System.gc(); //调用垃圾回收器,触发FULL GC
// 但也不是调用后立刻回收的,因为线程的执行权是在操作系统上
Thread.sleep(500);
//因为finalize方法的优先级太低,在这强制等待一下使finalize方法的运行,停止0.5秒
if (demo != null) {
System.out.println("我又回来了");
} else {
System.out.println("我死了");
}
//对象第二次拯救自己
System.out.println("第二次:");
demo = null;
System.gc();
Thread.sleep(500);
//因为finalize方法的优先级太低,在这强制等待一下使finalize方法的运行,停止0.5秒
if (demo != null) {
System.out.println("我又回来了");
} else {
System.out.println("我死了");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
从代码中运行结果可以明显的看出,demo的对象finalize()方法确实是可以被垃圾收集器触发的,并且在被收集前成功可以逃脱的。
在这个代码中也完整的体现出来,虽然运行了同样的代码,但是执行结果是一次成功一次失败,这也说明出来finalize方法是只能被调用一次的,如果再在下一次回收的时候,它的finalize方法是不会在被调用的。
最后再次说明一下,虽然Java拥有这样的机制,但是是完全不建议使用的,它的运行代价高,不确定性大,无法保证各个对象的调用顺序,在官方说明中都明确说明了不建议使用的。最开始做这个完全是为了使传统的C、C++程序员更容易接受Java所做的一种妥协。
而我们要做“关闭外部资源”之类的清理工作的时候,完全是可以采用try-finally等方式来使用的。
下一篇: >>>> 垃圾回收算法