最终化答应实现java对象的最后清理;但是,就算不显式地使用它,它也能延迟资源的回收。在本文中你还会学习如何避免类似的内存保留问题。
一、 引言
最终化是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()方法就是这样的一个例子)。
然而,你不可能总是用上面描述的方式重新安排你的代码。在这种情况下,作为一个类用户,你必须做点多余的工作来确保当它们被终结时其实例并不占有多余的空间。下列代码说明实现方法: