6.4 终结机制

有一种古老的资源管理技术叫终结(finalization),开发者应该知道有这么一种技术。然而,这种技术几乎完全废弃了,任何情况下,大多数 Java 开发者都不应该直接使用。

6.4 终结机制 - 图1 只有少数应用场景适合使用终结,而且只有少数 Java 开发者会遇到这种场景。如果有任何疑问,就不要使用终结,处理资源的 try 语句往往是正确的替代品。

终结机制的作用是自动释放不再使用的资源。垃圾回收自动释放的是对象使用的内存资源,不过对象可能会保存其他类型的资源,例如打开的文件和网络连接。垃圾回收程序不会为你释放这些额外的资源,因此,终结机制的作用是让开发者执行清理任务,例如关闭文件、中断网络连接、删除临时文件,等等。

终结机制的工作方式是这样的:如果对象有 finalize() 方法(一般叫作终结方法),那么不再使用这个对象(或对象不可达)后的某个时间会调用这个方法,但要在垃圾回收程序回收分配给这个对象的空间之前调用。终结方法用于清理对象使用的资源。

在 Oracle/OpenJDK 中,按照下述方式使用这种技术。

(1) 如果可终结的对象不可达了,会在内部终结队列中放一个引用,指向这个对象;而且,为了回收垃圾,这个对象会被标记为“存活”。

(2) 对象一个接着一个从终结队列中移除,然后调用各自的 finalize() 方法。

(3) 调用终结方法后,不会立即释放对象,因为终结方法可能会把 this 引用存储在某个地方(例如在某个类的公开静态字段中),让对象再次拥有引用,复活对象。

(4) 因此,调用 finalize() 方法后,垃圾回收子系统在回收对象之前,必须重新判断对象是否可达。

(5) 不过,就算对象复活了,也不会再次调用终结方法。

(6) 综上所述,定义了 finalize() 方法的对象一般(至少)会多存活一个 GC 循环(如果是生命期长的对象,会再多存活一个完整的 GC 循环)。

终结机制的主要问题是,Java 不确定什么时候回收垃圾,或者以什么顺序回收对象。因此,Java 平台无法确认什么时候(甚至是否)调用终结方法,或者以什么顺序调用终结方法。

因此,作为一种防止资源(例如文件句柄)稀少的自动清理机制,其设计是有缺陷的,因为不能保证终结机制运行得足够快,避免耗尽资源。

终结方法唯一真正有用的场景是,在一个类中使用本地方法,打开某个非 Java 资源。就算遇到这种情况,也更适合使用处理资源的块状 try 语句,但也可以声明一个 public native finalize() 方法(close() 方法会调用这个方法)——这个方法可以释放本地资源,包括不受 Java 垃圾回收程序控制的堆外内存。

终结机制的细节

为了少数适合使用终结机制的场景,下面列出一些额外细节,以及使用过程中的注意事项。

  • 在没有回收全部重要的对象之前,JVM 可能就会退出,所以根本不会调用某些终结方法。遇到这种情况,操作系统会关闭网络连接等资源,并将其回收。然而,要注意,如果要删除文件的终结方法没有运行,操作系统不会删除那个文件。

  • 为了确保在虚拟机退出前执行某些操作,Java 提供了 Runtime::addShutdownHook 钩子,在 JVM 退出前安全执行任意代码。

  • finalize() 方法是实例方法,作用在实例上。没有等效的机制用来终结类。

  • 终结方法是实例方法,没有参数,也不返回值。每个类只能有一个终结方法,而且必须命名为 finalize()

  • 终结方法可以抛出任何类型的异常或错误,但垃圾回收子系统自动调用终结方法时,终结方法抛出的任何异常或错误都会被忽略,这些异常或错误只会导致终结方法返回。