深入理解java的finalize

一.相关知识点

        1.java的GC只负责内存相关的清理,所有其它资源的清理必须由程序员手工完成。要不然会引起资源泄露,有可能导致程序崩溃。 

        2.调用GC并不保证GC实际执行。 

        3.finalize抛出的未捕获异常只会导致该对象的finalize执行退出。 

        4.用户可以自己调用对象的finalize方法,但是这种调用是正常的方法调用,和对象的销毁过程无关。 

        5.JVM保证在一个对象所占用的内存被回收之前,如果它实现了finalize方法,则该方法一定会被调用。Object的默认finalize什么都不做,为了效率,GC可以认为一个什么都不做的finalize不存在。 

        6.对象的finalize调用链和clone调用链一样,必须手工构造,如下所示: 

protected void finalize() throws Throwable {  
    super.finalize();  
}

二.对象的销毁过程 

        在对象的销毁过程中,按照对象的finalize的执行情况,可以分为以下几种,系统会记录对象的对应状态: 

        a.unfinalized 没有执行finalize,系统也不准备执行。 

        b.finalizable 可以执行finalize了,系统会在随后的某个时间执行finalize。 

        c.finalized 该对象的finalize已经被执行了。 

        GC怎么来保持对finalizable的对象的追踪呢。GC有一个Queue,叫做F-Queue,所有对象在变为finalizable的时候会加入到该Queue,然后等待GC执行它的finalize方法。 

        这时我们引入了对对象的另外一种记录分类,系统可以检查到一个对象属于哪一种。 

        a.reachable:从活动的对象引用链可以到达的对象,包括所有线程当前栈的局部变量,所有的静态变量等等。 

        b.finalizer-reachable除了reachable外,从F-Queue可以通过引用到达的对象。

        c.unreachable其它的对象。 

        来看看对象的状态转换图:

        1.首先,所有的对象都是从Reachable+Unfinalized走向死亡之路的。 

        2.当从当前活动集到对象不可达时,对象可以从Reachable状态变到F-Reachable或者Unreachable状态。 

        3.当对象为非Reachable+Unfinalized时,GC会把它移入F-Queue,状态变为F-Reachable+Finalizable。

        4.好了,关键的来了,任何时候,GC都可以从F-Queue中拿到一个Finalizable的对象,标记它为Finalized,然后执行它的finalize方法,由于该对象在这个线程中又可达了,于是该对象变成Reachable了(并且Finalized)。而finalize方法执行时,又有可能把其它的F-Reachable的对象变为一个Reachable的,这个叫做对象再生。 

        5.当一个对象在Unreachable+Unfinalized时,如果该对象使用的是默认的Object的finalize,或者虽然重写了,但是新的实现什么也不干。为了性能,GC可以把该对象直接变到Reclaimed状态直接销毁,而不用加入到F-Queue等待GC做进一步处理。 

        6.从状态图看出,不管怎么折腾,任意一个对象的finalize只至多执行一次,一旦对象变为Finalized,就怎么也不会在回到F-Queue去了。当然没有机会再执行finalize了。 

        7.当对象处于Unreachable+Finalized时,该对象离真正的死亡不远了。GC可以安全的回收该对象的内存了。进入Reclaimed。 

三.对象重生

        实例:

package com.bijian.study;

class C {
	static A a;
}

class A {
	B b;

	public A(B b) {
		this.b = b;
	}

	@Override
	public void finalize() {
		System.out.println("A finalize");
		C.a = this;
	}
}

class B {
	String name;
	int age;

	public B(String name, int age) {
		this.name = name;
		this.age = age;
	}

	@Override
	public void finalize() {
		System.out.println("B finalize");
	}

	@Override
	public String toString() {
		return name + " is " + age;
	}
}

public class Main {
	
	public static void main(String[] args) throws Exception {
		
		A a = new A(new B("allen", 20));
		a = null;

		System.gc();
		Thread.sleep(5000);
		System.out.println(C.a.b);
	}
}

        期待的运行结果:

B finalize
A finalize
allen is 20

        但是有可能失败。

Exception in thread "main" java.lang.NullPointerException
	at com.bijian.study.Main.main(Main.java:51)

        因为GC的不确定性以及时序问题,详细解释见http://bijian1013.iteye.com/blog/2289781

        但我在JDK1.8上DEBUG运行结果可以确认的是,如果垃极回收的Daemon线程一直执行的话,肯定会先执行关联对象B的finalize()方法,再执行A对象的finalize()方法。

四.对象的finalize的执行顺序

        所有finalizable的对象的finalize的执行是不确定的,既不确定由哪个线程执行,也不确定执行的顺序。考虑以下情况就明白为什么了,实例a,b,c是一组相互循环引用的finalizable对象。 

五.何时及如何使用finalize

        从以上的分析得出,以下结论:

        1.最重要的,尽量不要用finalize,太复杂了,还是让系统照管比较好。可以定义其它的方法来释放非内存资源。 

        2.如果用,尽量简单。 

        3.如果用,避免对象再生,这个是自己给自己找麻烦。 

        4.可以用来保护非内存资源被释放。即使我们定义了其它的方法来释放非内存资源,但是其它人未必会调用该方法来释放。在finalize里面可以检查一下,如果没有释放就释放好了,晚释放总比不释放好。 

        5.即使对象的finalize已经运行了,不能保证该对象被销毁。要实现一些保证对象彻底被销毁时的动作,只能依赖于java.lang.ref里面的类和GC交互了。 

六.参考 

        关于引用类型,GC、finalize的相互交互可以参考ReferenceQueue GC finalize Reference 测试及相关问题

文章来源:http://zhang-xzhi-xjtu.iteye.com/blog/484934

猜你喜欢

转载自bijian1013.iteye.com/blog/2289661