10.4 对象终止

对象创建完成后,使用一段时间就可能不再需要了。如果没有引用指向一个对象,说明该对象可以被销毁。在创建和使用对象的过程中,可能申请了相关的资源,在对象被销毁之前,这些资源要被正确地释放。资源分成内存资源和非内存资源两类。内存资源指的是对象中实例域所占据的内存空间。由于Java采用了自动内存管理机制,对象占用的内存资源的回收由垃圾回收器自动完成,不需要开发人员显式地进行内存释放。非内存资源指的是程序运行时申请的其他系统资源,包括打开的文件、打开的套接字连接和数据库连接等。这些非内存资源需要由程序显式地进行释放。

与C++语言进行对比可以发现,在Java中,对这两种资源的释放操作是分开处理的,而在C++语言中,对这两种资源的释放方式是统一的,都在析构方法中进行。在Java中,没有析构方法的概念,同时,对非内存资源的释放又无法以自动的方式来进行,因此,Java引入了对象终止机制(finalization)来解决非内存资源的释放问题。但是由于设计上的各种问题和功能上的局限性,对象终止机制并没有发挥应有的作用。

1.finalize方法的基本用法

如果一个Java类的对象有自定义的销毁逻辑,那么可以覆写Object类的finalize方法并在finalize方法中添加相关的逻辑。在一个对象的内存空间被垃圾回收器回收之前,该对象的finalize方法会被调用。finalize方法中的这一段处理逻辑称为对象的终止器(finalizer)。Java的对象终止机制看起来比较有用,但是在实际的程序中并不实用。最主要的原因是Java语言规范并没有对finalize方法的调用时间进行明确的规定,只是规定finalize方法一定在对象的内存空间被垃圾回收器回收之前运行。第7章介绍垃圾回收器时提到过,垃圾回收器的运行时间是不固定的,因此一个对象被回收的时间也是不确定的。这双重的不确定性导致无从得知finalize方法的具体运行时间。这就意味着如果把非内存资源的释放操作放在finalize方法中,那么该资源的实际释放时间是不固定的,从而可能产生与时间相关的错误。如果在某个时间点上finalize方法碰巧被执行了,那么程序的行为是正确的;如果finalize方法没有被执行,则可能资源没被正确释放。这种随机错误显然是不能出现在程序中的。

代码清单10-6中给出了运行finalize方法的示例。RunFinalize类通过覆写finalize方法提供了自定义的对象终止逻辑。如果finalize方法被运行了,那么会在控制台输出提示信息。在代码运行时首先创建了一个RunFinalize类的对象,然后显式地把对象引用设为null,使该对象可以被垃圾回收。如果程序运行到此处就结束,那么会发现finalize方法并没有被运行。这是因为垃圾回收器并没有运行,也就不可能调用对象的finalize方法。在后面的代码中通过System.gc方法来建议垃圾回收器运行并等待一段时间。通过这种方式可以增加垃圾回收器运行的几率,也使创建的RunFinalize类的对象有机会被回收。添加这样的逻辑之后,finalize方法被运行的几率大大增加。

代码清单10-6 运行finalize方法的示例


public class RunFinalize{

protected void finalize()throws Throwable{

System.out.println("运行finalize方法。");

super.finalize();

}

public static void main(String[]args)throws InterruptedException{

RunFinalize runFinalize=new RunFinalize();

runFinalize=null;

for(int i=0;i<10;i++){

System.gc();

Thread.sleep(100);

}

}

}


需要注意的是,虽然通过System.gc方法可以增加垃圾回收器运行的几率,进而增加finalize方法被运行的几率,但是finalize方法的实际运行时间仍然是不能保证的。代码清单10-6只是为了说明finalize方法的运行时机与垃圾回收器的关系,不应该作为实际程序中的处理方式。

2.finalize方法与资源释放

在实际的程序中,不应该仅依靠对象的finalize方法来进行非内存资源的释放。例如,在一个对象的使用过程中打开了多个文件,如果把文件的关闭操作放在finalize方法中进行,则可能会出现问题。代码清单10-7中给出了一个错误使用finalize方法的示例。在类FileHolder的构造方法中传入一个要打开的文件的路径,在open方法中打开该文件得到一个InputStream类的对象。需要对打开的文件执行正确的关闭操作。FileHolder类把InputStream类的对象的关闭操作放在了finalize方法中。这样做时finalize方法的运行时间是不确定的,有可能出现的情况是,程序中存在大量FileHolder类的对象,而这些对象的finalize方法都没有被调用,导致大量处于打开状态的文件没有被关闭。操作系统对同时打开的文件数量是有限制的,大量打开文件会造成程序运行出现严重问题。

代码清单10-7 错误使用finalize方法来关闭文件的示例


//错误的finalize使用

public class FileHolder{

private Path path;

private InputStream inputStream;

public FileHolder(Path path){

this.path=path;

}

public void open()throws IOException{

this.inputStream=Files.newInputStream(path, StandardOpenOption.WRITE);

}

protected void finalize()throws Throwable{

if(inputStream!=null){

inputStream.close();

}

super.finalize();

}

}


正确释放非内存资源的做法应该是在类中添加显式释放资源的方法,由对象的使用者负责调用。Java类可以实现Java 7中新增的java.lang.AutoCloseable接口,进而可以通过调用close方法或者使用更加简便的try-with-resources语句来进行资源释放。对于代码清单10-7中的FileHolder类,正确的做法是实现AutoCloseable接口,并在close方法中关闭InputStream类的对象。提供显式的资源释放方法相当于把资源释放的职责交给了对象的使用者。这要求使用者在编写代码时注意在合适的时机释放所申请的非内存资源。在添加了显式的资源释放方法之后,也可以在finalize方法中添加对这个方法的调用。这样做的好处是,即使对象的使用者忘记调用释放资源的方法,也可能有机会释放这个资源。

3.实现正确的finalize方法

在finalize方法的声明中,finalize方法可能抛出任何类型的异常。如果finalize方法在执行时出现异常,则抛出的异常会直接被忽略,对finalize方法的调用也马上终止。由于finalize方法是由虚拟机来直接调用的,因此无法在代码中捕获finalize方法中抛出的异常,也不会有异常的堆栈信息被输出。

在自定义的finalize方法的实现中,总是应该调用父类的finalize方法。这是因为在对象创建时,会依次调用父类的构造方法来完成对象的初始化。与之相对应的是,在对象被回收之前,父类的终止逻辑也要被调用。但是与构造方法不同的是,父类的finalize方法不会被自动调用。因此,在finalize方法的实现中,先编写当前类的终止逻辑,再通过super.finalize()来调用父类的finalize方法。由于在Object类中定义了finalize方法,因此super.finalize()的调用总是合法的。为了保证父类的finalize方法总是被调用,需要把当前类的终止逻辑封装在一个try-finally结构中。不管当前类的终止逻辑是否成功完成,父类的finalize方法始终会被调用。

如果当前Java类通过覆写finalize方法添加了相关的对象终止逻辑,同时该类的子类也覆写了finalize方法,则子类的finalize方法应该调用父类的finalize方法。但是由于子类的实现不由当前Java类控制,因此可能会因为开发人员的错误而造成当前类的finalize方法没有被调用。为了避免这种情况,可以使用一种被称为“终止器守卫者(finalizer guardian)”的模式。如代码清单10-8所示,WithFinalizer类有自定义的对象终止逻辑,但是这些代码没有添加在WithFinalizer类的finalize方法中,而添加在WithFinalizer类的一个实例域guardian的finalize方法中。当WithFinalizer类的对象可以被回收时,guardian对象也同样可以被回收。此时guardian对象的finalize方法会被调用,完成WithFinalizer类的对象的终止逻辑。当使用这种模式时,即便WithFinalizer类的子类没有调用super.finalize方法,WithFinalizer类的对象也能被正确终止。

代码清单10-8“终止器守卫者(finalizer guardian)”模式的示例


public class WithFinalizer{

private final Object guardian=new Object(){

protected void finalize()throws Throwable{

//WithFinalizer类的对象终止实现

super.finalize();

}

};

}


在finalize方法的实现中要避免创建对当前对象的新的引用,不论是直接引用还是间接引用,都要避免。创建新的引用会造成当前对象从没有引用的状态又回到有引用的状态。不过当对象再次变成没有引用的状态时,该对象的finalize方法不会被再次调用。一个对象的finalize方法只会被调用一次。