第7条 避免使用终结方法

1. 什么是终结方法

JVM垃圾处理器在回收对象内存前需要做一些操作,以确保该对象正在处理的非Java资源被释放,如文件句柄、数据库连接等。为处理这样的状况,Java 提供了被称为收尾(finalization )的机制,通常就是给类定义finalize()方法,就是我们所说的终结方法。使用该机制你可以定义一些特殊的操作,这些操作在一个对象将要被垃圾回收程序释放时执行,因为垃圾回收该对象前会调用其所属类的finalize()方法(如果有定义的话)。
但是终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。Java提供终结方法finalize(),Object类中
 protected void finalize() throws Throwable { }

2. 终结方法的缺点

 1)不能保证会被及时地执行。

Java语言规范不仅不保证终结方法会被及时地执行,而且根本就不保证它们会被执行,因此不应该依赖终结方法来更新重要的持久状态。例如,依赖终结方法释放数据库连接、关闭打开的文件等。
一个对象变得不可达开始到它的终结方法被执行,所花费的时间是任意长的,因为JVM内存足够的话不一定会触发垃圾回收,即JVM有可能会延迟执行终结方法,但是这种延迟不是"有意的"。因此终结方法的执行时间点是非常不稳定的。
不同JVM的垃圾回收算法不同,程序的行为在不同的JVM中运行表现可能截然不同,比如一个程序在你的JVM平台上运行得好好的,但是在你同学的JVM平台上却根本无法运行。

 2)线程优先级低。

也会影响终结方法的执行时刻,因为垃圾回收器线程优先级低,它只有等所有线程都挂起等待该内存释放的时候才会开始释放该对象的内存,此时才会执行finalize()方法

 3)严重的性能损失。

3. 终结方法的用处

  1)充当"安全网(safty net)"

当对象所有者忘记调用显示终止方法(见4)时,终结方法可以充当最后一道屏障,可能在某个时刻可以会触发并释放关键资源,迟一点释放资源总比不释放好。如果发现终结方法发现资源还未被终止,应该在日志中记录一条警告

2)与对象的本地对等体(native peer)有关。

本地对等体对象是一个本地对象(native object),普通对象通过本地方法(native method)委托而生成的一个本地对象。因为本地对等体不是一个普通对象,所以垃圾回收器不会知道它,当它的Java对等体被回收的时候,它不会被回收。因此,如果本地对等体未持有关键资源的时候,终结方法是回收本地对等体最合适的工具;但是如果本地对等体持有必须及时终止的资源,那么该类就应该具有一个显示的终止方法。

4. 终结方法的正确使用

前面提到一般情况下不要使用终结方法,但是终结方法也是有一定用处的。如果类的对象中封装的资源(如文件或线程)确实需要终止,应该怎么做呢?

1) 提供一个显示的终止方法。

这里姑且将显示的终止方法也作为终止方法的一种使用方法。
对象所属类提供一个显示的终止方法,并要求该类的客户端在每个实例不再有用的时候调用这个方法,这种方法可以避免编写终结方法finalize()。
显示终止方法的典型例子是Java IO相关InputStream、OutputStream和数据库相关java.sql.Connection,以及各种连接池的close方法,另一个例子是java.util.Timer上的cancel方法。常配合try …catch …finally使用

InputStream inputStream = new FileInputStream();
try{
    //Do something about inputStream;
}finally{
    inputStream.close();
}
2)子类的终结方法必须手动调用父类终结方法

非常重要的一点是"终结方法链"并不会被自动执行。如果父类有终结方法且子类覆盖了父类终结方法,则子类的终结方法中必须要显示调用父类的终结方法。这样做可以保证:即使子类终结过程抛出异常,父类终结方法也会得到执行。

@Override 
protected void finalize() throws Throwable { 
	try{ 
	...//Finalize subclass state 
	} finally { 
	super.finalize(); 
	} 
}

如果子类覆盖了父类的终结方法,但是却忘了手动调用父类的终结方法,那么父类的终结方法将永远也不会被执行。为了防范这样的情况,我们引出第三种方法。

3)终结方法守卫者(finalizer guardian)

终结方法守卫者其实就是该对象的私有匿名内部类实例,唯一的用途就是终结它的外围实例,即使子类的终结方法中没有执行super.finalize()。

public class Foo {

    public static void main(String[] args){
        doSth();
        System.gc();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void doSth() {
        Child c = new Child();
        System.out.println(c);
    }

    private final Object guardian = new Object(){
         @Override
         protected void finalize(){
             System.out.println("执行父类中匿名内部类--终结方法守卫者,重写的finalize()");
             // 在这里调用Parent重写的finalize即可在清理子类时调用父类自己的清理方法
             parentlFinalize();
         }
     };

    protected void parentlFinalize() {
        System.out.println("执行父类自身的终结方法");
    }
}

class Child extends Foo {
    @Override
    protected void finalize() {
        System.out.println("执行子类finalize方法,注意,这里子类并没有调用super.finalize()");
        // 由于子类(忘记或者其他原因)没有调用super.finalize()
        // 使用终结方法守卫者可以保证子类执行finalize()时(没有调用super.finalize()),父类的清理方法仍旧调用
    }
}

在这里插入图片描述
这里有个疑问:从执行结果看似乎没啥毛病,但是调用子类的finalize()终结方法时,是怎么会触发父类的匿名内部类的内部方法finalize()执行的呢?是JVM或者Java规范么?求知道的大神在评论区可以帮忙解答

5. 总结

除非是作为安全网或终止非关键的本地资源,否则请尽量避免使用终结方法。当然,如果使用文件、数据库连接或其他连接池等确实需要终止的资源,可以选择提供一个显示的终结方法,类似于InputStream的close方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值