一、 引言
最终化是Java编程语言的一个特性:它允许你对垃圾收集器发现的不可达的对象进行最后清理。典型地用于回收与一对象相关联的本地资源。下面是一个简单的最终化示例:
public class Image1 { //指向本地图像数据 private int nativeImg; private Point pos; private Dimension dim; //它释放本地图像; //随后对它的调用将被忽略 private native void disposeNative(); public void dispose() { disposeNative(); } protected void finalize() { dispose(); } static private Image1 randomImg; } |
图1.一个可最终化的对象 |
有时,在一个Image1实例变为不可达的时,Java虚拟机(JVM)将调用它的finalize()方法来确保含有图像数据(在本例中被整数nativeImg所指向)的本地资源已经被回收。然而,请注意,该finalize()方法,先不管它被JVM进行专门对待,是一个任意的方法-它包含任意的代码。特别地,它能存取任何对象中的任何字段(在本例中指pos和dim)。令人惊讶的是,它还能使该对象再次成为可达的-比如说通过让它从一个静态字段成为可达的(如,randomImg=this;)。我根本不推荐使用后面这种编程实践,但是遗憾的是,Java编程语言允许它。
下面步骤描述一个可最终化的对象obj的生命周期-也即,这是一个其类中有一个非平凡的(non-trivial)终结器的对象(见图1):
图2.垃圾收集器确定这个obj是不可达的 |
1. 当obj被分配时,JVM内部地记录下这个obj是可最终化的(这在典型情况下会减慢现代JVM具有的其它方面的分配路径)。
2. 当垃圾收集器确定该obj是不可达的时,它注意到,这个obj是可最终化的(因为它在分配时就被记录下来)并且把它添加到JVM的最终化队列上。它还确保从obj可达的所有对象被保留起来,即使它们从其它对象也许是不可达的,因为它们可能会被终结器所存取。图2展示了Image1的一个实例的情况。
3. 在后面的时候,JVM的终结器线程将出队obj,调用它的finalize()方法,并且记录下该obj的终结器已经被调用。此时,obj被认为是被最终化的。
4. 当垃圾收集器再次发现该obj是不可达的时,它将连同一切它所可达的(假定后者是不可达的)对象回收它的空间。
注意,垃圾收集器至少需要两个周期(也许更多)来回收obj并且需要保留在该过程中所有另外的从obj可达的对象。如果一个程序员不小心,那么这可能会创建暂时的、微妙的和无法预言的资源保留问题。另外,JVM并不保证它将调用所有的已分配的可最终化的对象的终结器;它可能在垃圾收集器发现它们其中一些是不可达的之前就已退出。
二、 在子类化时避免内存保留问题
就算你不显式地使用它,最终化也可能延期资源的回收。请考虑下列实例:
public class RGBImage1 extends Image1 { private byte rgbData[]; } |
RGBImage1扩展Image1并且引入了新字段rgbData(也许还有一些方法,而本示例中却没有显示)。尽管你没有显式地在RGBImage1上定义一终结器,但是,这个类将自然地继承Image1的finalize()方法,并且所有的RGBImage1实例也将被认为是可最终化的。当一个RGBImage1实例成为不可达的,回收可能的很大的rgbData数组将被延迟直到该实例被终结(见图3)。这可能是一个很难发现的问题,因为该终结器可能是隐藏在一个很深的类层次上。
一种避免这个问题的方法是重新安排代码,这样它可以使用"包含"来代替"扩展"模式,如下所示:
public class RGBImage2 { private Image1 img; private byte rgbData[]; public void dispose() {img.dispose();} } |
图3.GC将因最终化而只排队Image1实例 |
与RGBImage1相比,RGBImage2包含一个Image1的实例而不是扩展Image1。当RGBImage2的一个实例成为不可达时,垃圾回收器将即时回收它,连同rgbData数组(假定后者从任何其它地方都是不可达的),并且在最终化时将只排队Image1实例(见图4)。既然类RGBImage2并没有子类化Image1,那么它就不会从它中继承任何方法。因此,你可能必须把delegator方法添加到RGBImage1以存取要求的Image1中的方法(dispose()方法就是这样的一个例子)。
然而,你不可能总是用上面描述的方式重新安排你的代码。在这种情况下,作为一个类用户,你必须做点多余的工作来确保当它们被终结时其实例并不占有多余的空间。下列代码说明实现方法:
public class RGBImage3 extends Image1 { private byte rgbData[]; public void dispose() { super.dispose(); rgbData = null; } } |
图4.在使用一个RGBImage3实例后调用dispose() |
RGBImage3与RGBImage1相同,但是添加了dispose()方法-它用来把rgbData字段置为null。你需要显式地在使用完一个RGBImage3实例之后调用dispose()以保证rgbData数组被即时回收(见图4)。我推荐在极少的场合下显式地把字段置为null;这里就是其中之一。
三、保护用户免于内存保留问题
前一节描述了在用使用终结器的第三方类工作时怎样避免内存保留问题。本节将描述怎样创建需要最后清理的类,这样以来它们的用户就不会遇到前面所概括的问题。为此,最好的方法是把这样的类分解为两个(一个持有需要最后清理的数据,另一个持有其它一切)并且只在前者上定义一个终结器。下面的代码展示了这一技术:
final class NativeImage2 { private int nativeImg;//指向本地图像数据 //它释放本地图像;随后对它的调用将被忽略 private native void disposeNative(); void dispose() { disposeNative(); } protected void finalize() { dispose(); } } public class Image2 { private NativeImage2 nativeImg; private Point pos; private Dimension dim; public void dispose() { nativeImg.dispose(); } } |
图5.当Image2实例成为不可达时,只有NativeImage2实例将会排队 |
Image2相似于Image1,但是它的nativeImg字段被包含在一个独立的类NativeImage2中。所有从图像类到nativeImg的存取必须经由一个重定向层。然而,当一个Image2实例成为不可达的时候,只有NativeImage2实例将排队等待最终化;任何其它从Image2实例可达的都将被提示回收(见图5)。类NativeImage2被声明为final,这样用户就不可能把它子类化并且重新引入了前一节所描述的内存保留问题。
一处微妙的地方在于,NativeImage2不应该成为一个Image2的内部类。内部类的实例都有一个到创建它们的外部类的实例的隐含参考。所以,如果NativeImage2是Image2的一个内部类,并且一个NativeImage2实例在排队等待最终化,它应该保留相应的Image2实例,这恰恰是前面你尽力想避免的。然而,假定NativeImage2类只能从Image2类中进行存取。这就是为什么它没有公共方法的原因(它的dispose()方法,以及类本身都是为包所私有的)。
四、 一种代替最终化的选择
在前面一节中的示例还存在一种不确定性可能:JVM并不能保证它在最终化队列中调用对象的终结器的顺序。而来自于所有类(应用程序,库,等等)的终结器都是被同等对待的。因此,一个占有大量内存或一种稀有的本地资源的对象可能受阻于终结化队列-它们排在那些终结器进度缓慢的对象之后(不一定是恶意;也许由于懒惰的编程所致)。
为了避免这种类型的不确定性,你可以使用弱参考来代替最终化,例如使用死后钩子(postmortem hook)。如果用这种方式,你可以完全控制怎样优先化本地资源的回收问题,而代替依赖于JVM完成这件事情。下面的示例展示了这一技术:
final class NativeImage3 extends WeakReference<Image3> { private int nativeImg;//指向本地图像数据 //它释放本地图像;随后对它的调用将被忽略 private native void disposeNative(); void dispose() { disposeNative(); refList.remove(this); } static private ReferenceQueue<Image3> refQueue; static private List<NativeImage3> refList; static ReferenceQueue<Image3> referenceQueue() {return refQueue;} NativeImage3(Image3 img) { super(img, refQueue); refList.add(this); } } public class Image3 { private NativeImage3 nativeImg; private Point pos; private Dimension dim; public void dispose() { nativeImg.dispose(); } } |
Image3与Image2相同。NativeImage3相似于NativeImage2,但是它的最后清理依赖于弱参考而不是最终化。NativeImage3扩展WeakReference,其参考是与之相关联的Image3实例。请记住,当一个参考对象的参考(此时是WeakReference)成为不可达的时,该参考对象就被添加到与之相关联的参考队列上。把nativeImg嵌入到参考对象本身就保证JVM会正确地把所需要的加入到队列中(见图6)。再强调一下,NativeImage3不应该成为Image3的一个子类,这是基于前面所述原因。
图6.把nativeImg嵌入到Reference对象本身 |
你可以决定是否一参考对象的参考物已经被垃圾收集器以两种方式回收:显式地,在参考对象上调用get()方法;隐式地,通过观察参考对象已经在相关联的参考队列中排队来实现。本示例中只使用了后者。
注意,参考对象仅能被垃圾收集器所发现并且被添加到它们的相关联的参考队列-只有它们本身是可达的时候。否则,它们就象任何其它不可达的对象一样被简单地回收。这就是为什么你把所有的NativeImage3实例添加到该静态链表(实际上,任何数据结构都会满足):为了确保它们保持为可达的并且当它们的参考物成为不可达的时被处理。当然,你还必须确保当你释放它们时(这是通过dispose()方法来实现的)你从该列表中删除了它们。
当在一个Image3实例上显式地调用dispose()方法时,在该实例上不会发生随后的最后清理;正确情况下也是这样,因为这里不需要任何东西。这个dispose()方法从静态列表中删除NativeImage3实例,这样当它的相应的Image3实例成为不可达的时它就是不可达的。并且,如前所述,不可达的参考对象并不被添加到它们相应的参考队列。相反,在所有前面的使用了最终化的示例中,可最终化的对象将总是被作最终化考虑-当它们成为不可达的时候,无论你是否已显式地释放它们相关联的本地资源。
JVM将保证,当通过垃圾收集器发现一个Image3实例是不可达的时候,它会把它的相应的NativeImage3实例添加到它的相关联的参考队列上去。然后,由你负责把它从队列中删除并释放它的本地资源。这可以通过在一个"清理"线程中,用一个如下的循环来实现:
ReferenceQueue<Image3> refQueue =NativeImage3.referenceQueue(); while (true) { NativeImage3 nativeImg =(NativeImage3) refQueue.remove(); nativeImg.dispose(); } |
这是一个过于简单的实例。高级开发者能另外根据它们如何需要优先化清理来确保不同参考对象关联于不同的参考队列。并且一个单个的"清理"线程可以查询所有可用的参考队列和根据要求的优先级来从队列中删除对象。另外,你可以选择展开(spread out)回收资源,这样它就会给应用程序带来更少的危险性。
尽管用这种方式清理资源与使用最终化相比,明显是更复杂些,但是这也是一种更为有力量和更为灵活的方式,而且可以最小化大量的与使用最终化相关的不确定性。另外,这种方式还十分相似于最终化实际在JVM内实现的方式。对于那些显式地使用很多本地资源并且需要更多控制的工程,我推荐对它们进行清理时使用这一方法。而只要小心地使用最终化对于大多数另外的工程来说也就足够了。
注意:本文仅讨论了两种类型的在使用最终化时产生的问题,也就是内存和资源保留问题。最终化和参考类的使用也能带来很微妙的同步问题。要想详细了解这一点,可以参考Read Hans-J.Boehm的《最终化,线程和基于Java技术的内存模型》一文。
五、仅在必要时才使用最终化
本文简短描述了最终化是怎样在JVM中实现的。然后给出了有关内存是怎样不必被可最终化的对象所保留的示例并且概括了这种问题的解决方案。最后,我描述了一个方法-它使用弱参考来代替-这允许你用一种更为灵活和可预测的方式来执行最后清理。
然而,完全依赖于垃圾收集器来识别不可达的对象以便与它们相关联的本机和潜在的较为缺乏的资源就可以被回收存在一个严重的不足:典型的情况下,内存是丰富的,而用一种丰富的资源来保护一种潜在地缺乏的资源并不是一个好策略。因此,如果你使用一个你知道它有与之相关联的本地资源(例如,一个GUI组件,文件,套接字)的对象,那么在使用完之后,一定要调用它的dispose()或equivalent方法。这将保证立即回收本地资源并且减小这些资源流失的可能性。通过这种方式,你可以使用本文中所讨论的方法来作为补救性的最后清理而不是作为主要的清除机制。你还应该尽量限制你的最终化使用-仅在绝对必要时使用之。总之,最终化是一个不确定的和有时无法预言的过程。你越少地依赖于它,它对JVM和你的应用程序就有越小的影响。