三、保护用户免于内存保留问题
前一节描述了在用使用终结器的第三方类工作时怎样避免内存保留问题。本节将描述怎样创建需要最后清理的类,这样以来它们的用户就不会遇到前面所概括的问题。为此,最好的方法是把这样的类分解为两个(一个持有需要最后清理的数据,另一个持有其它一切)并且只在前者上定义一个终结器。下面的代码展示了这一技术:
| 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和你的应用程序就有越小的影响。
[首页] [上一页] [下一页] [末页]


