Java Solaris 加入 SDN 参与讨论 我的社区 注册说明
 
 
 
 
 
 
Java API 文档中文版
如何处理 Java Finalization 的内存保留问题
 
By Tony Printezis, 1/21/09  
Finalize() 是 Java 编程语言提供的一种特殊方法。当垃圾收集器发现某个对象无法触及时,我们便可以使用该方法事后释放对象占用的内存。它通常用于回收与某个对象相关的本地资源。下面是 Finalize() 方法的一个简单示例:
public class Image1 {

// pointer to the native image data
private int nativeImg;
private Point pos;
private Dimension dim;

// it disposes of the native image;
// successive calls to it will be ignored
private native void disposeNative();
public void dispose() { disposeNative(); }
protected void finalize() { dispose(); }

static private Image1 randomImg;
}
 

Image1 实例变为不可触及状态时,Java 虚拟机(JVM) *将调用其 finalize() 方法确保存储图像数据的本机资源(在本例中为 int 类型的 nativeImg)已被回收。

但是需要注意,虽然 finalize() 方法在 JVM 中被特殊对待,但是它可以包含任意代码。特别是,它可以访问对象中的任何字段——在本例中为 posdim。令人惊讶的是,它还可以使对象重新变为可触及状态,比如说从静态字段变为可触及状态(如 randomImg = this;)。虽然并不推荐使用后面这种编程方式,但不幸的是 Java 编程语言还是允许这样做。

以下步骤和图 1 描述了 finalizable 对象 obj(即拥有非平凡 finalizer 的对象)的生存期。

Lifetime of Finalizable Object obj
图1. finalizable 对象 obj 的生存期。
 
  1. 当为 obj 对象分配内存时,JVM 将在内部记录 obj 可执行 finalize。一般而言,这将减缓现代 JVM 具有的快速分配方法。
  2. 当垃圾收集器确认 obj 不可触及,它将注意到 obj 是可执行 finalize 的(因为它在分配时已经记录)并将其添加到 JVM 的 finalization 序列。同时,它将确保从 obj 可触及的所有对象都保留在内存中,即使从它们从别处是不可触及的,因为 finalizer 可以访问它们。图 2 演示了 Image1 对象的一个实例。
     
    Garbage Collector Determines That obj Is Unreachable.
    图 2. 垃圾收集器判断 obj 不可触及。
     
  3. 然后,JVM 的 finalizer 线程 将从序列中解除 obj,调用其 finalize() 方法并记录 obj 的 finalizer 已经调用。这时,便认为 obj 已执行 finalized
     
  4. 当垃圾收集器再次发现 obj 不可触及时,它将回收该对象的空间以及通过它可触及的所有内容的空间,前提是后者在别处不可触及。

注意,垃圾收集器回收 obj 对象至少需要两个周期,并且在此过程中需要将 obj 可触及的所有其他对象保留在内存中。如果程序员没有注意到这点,则有时会造成临时的、微小的、不可预知的资源保留问题。此外,JVM 并不能保证它将调用已分配内存的所有 finalizable 对象的 finalizer。垃圾收集器发现其中一些对象不可触及之前,程序可能就已经退出。

避免子类继承时的内存保留问题

Finalization 可以延缓资源的回收,即使并不明确使用它。请考虑以下示例:

public class RGBImage1 extends Image1 {

private byte rgbData[];
}
 

RGBImage1 类继承 Image1 类并引入了字段 rgbData —— 可能还有一些方法,本例并没有显示。尽管没有在 RGBImage1 中明确定义一个 finalizer,但是该类将自动继承 Image1 中的 inalize() 方法,并且所有 RGBImage1 实例都将被认为是 finalizable。当 RGBImage1 实例不可触及时, rgbData 数组(占用内存很大)的回收将被延缓至该实例得到 finalized 时,如图 3 所示。内存保留问题有时难以发现,因为 finalizer 可能会“隐藏”在 很深的类层次结构中。

Reclamation of rgbData Array Will Be Delayed Until the Instance Is Finalized.
图 3. rgbData 数组的回收将延缓到实例执行 Finalize 时。
 

避免这种问题的方法之一便是重新安排代码,使用组成法(composition)替代继承,如下所示:

public class RGBImage2 {

private Image1 img;
private byte rgbData[];

public void dispose() {
img.dispose();
}
}
 

请阅读由 Joshua Bloch 的撰写的教程: Effective Java Programming Language Guide ,第 4 章项目 14:Favor composition over inheritance。

RGBImage1 相比, RGBImage2 包含一个 Image1 实例,而没有继承 Image1。当某个 RGBImage2 实例变为不可触及状态时,垃圾收集器将迅速回收该实例的空间以及 rgbData 数组的空间(假设后者从别处不可触及),并且只有 Image1 实例将进入 finalization 序列,如图 4 所示。由于类 RGBImage2 并不是 Image1 的子类,因此并没有从该继承任何方法。所以,我们可以在 RGBImage1 中添加一个委托方法用于访问 Image1 中的所需方法。 dispose() 方法就是这样的一个例子。

GC Will Queue Up Only the Image1 Instance for Finalization.
图 4. GC 只将 Image1 实例排入 Finalization 序列。
 

但是,也不能总是采用刚才的方式重新编写代码。有时,作为类的使用者,您不得不再添加一些内容,以确保其实例在 finalized 的时候不会占用多余的空间。以下代码演示了如何实现这一点:

public class RGBImage3 extends Image1 {

private byte rgbData[];

public void dispose() {
rgbData = null;
super.dispose();
}
}
 

RGBImage3 等同于 RGBImage1 ,但是多了一个 dispose() 方法。该方法用于将 rgbData 字段置为 null。使用完 RGBImage3 实例之后,我们需要明确调用 dispose() 方法以确保迅速回收 rgbData 数组的内存空间,如图 5 所示。明确将字段置空一般不是一个好的方式,但此处是一个例外:

Call dispose() After Using an RGBImage3 Instance.
图 5. 使用完 RGBImage3 实例后调用 dispose() 方法。
 
使用户远离内存保留问题

本文已经介绍了在操作使用 finalizer 的第三方类时如何避免内存保留问题。现在,我将介绍如何编写需要事后清除的类,以帮助用户避免上述问题。最佳的方法是将类一分为二( 一个用于保存需要事后清除的数据,另一个用于保存其他内容),并只对前者定义一个 finalizer。以下代码演示了此技巧:

final class NativeImage2 {

// pointer to the native image data
private int nativeImg;

// it disposes of the native image;
// successive calls to it will be ignored
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(); }
}
 

Image2 实例类似于 Image1,但是将 nativeImg 字段包含在一个单独的类 NativeImage2 中。从 image 类对 nativeImg 的所有访问都必须经过一个间接层。但是,当某个 Image2 实例变为不可触及状态时,只有 NativeImage2 实例将进入 finalization 序列。从 Image2 实例可触及的任何内容将迅速回收,如图 6 所示。类 NativeImage2 声明为 final 属性,因此用户不能创建该类的子类,并且再次引入了之前所讨论的内存保留问题。

When the Image2 Instance Becomes Unreachable, Only the NativeImage2 Instance Will Be Queued Up.
图 6. Image2 实例不可触及时,只有 NativeImage2 实例将进入 Finalization 序列。
 

这里有一个细节需要注意, NativeImage2 不可以是 Image2 的内部类。内部类的实例会隐式引用创建它们的外部类实例。因此,如果 NativeImage2Image2 的内部类,并且 NativeImage2 实例在 finalization 序列中,那么也会将相应的 Image2 实例保留,而这正是我们之前所试图避免的。但是,假设 NativeImage2 类只能从 Image2 类访问。这正是它没有公共方法的原因所在。它的 dispose() 方法和类本身都是包私有(package-private)的。

Finalization 的替代方案

上一个示例仍然存在一个不确定的问题:JVM 不能保证调用 finalization 序列中的对象的 finalizer 的顺序。并且所有类(应用程序、库等等)中的 finalizer 都是平等对待的。因此占用大量内存或本机资源的对象可能会一直保留在 finalization 序列中,从而拖慢系统。这不一定是恶意的,可能是在编写程序时粗心大意造成的。

要避免这种类型的不确定性,可以使用弱引用替代 finalzation 作为事后通知机制。这样,便可完全控制如何排列本地资源回收的优先级,而不用依赖 JVM 来完成这些任务。以下示例演示了此技巧:< /p>

final class NativeImage3 extends WeakReference<Image3> {

// pointer to the native image data
private int nativeImg;

// it disposes of the native image;
// successive calls to it will be ignored
private native void disposeNative();
void dispose() {
refList.remove(this);
disposeNative();
}

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 等同于 image2NativeImage3 类似于 NativeImage2,但是它的事后消除依赖于弱引用而不是 finalization。 NativeImage3 扩展了 WeakReference,它的指示物(referent)为与之关联的 Image3 实例。记住,当引用对象的指示物(在本例中为 WeakReference)不可触及时,引用对象将被添加到与之关联的引用序列。将 nativeImg 嵌入引用对象可确保 JVM 正确对序列排序。参见图 7。再提醒一次, NativeImage3 应该是 Image3 的内部类,原因同上。

Embedding nativeImg into the Reference Object Itself.
图 7. nativeImg 嵌入引用对象本身。
 

可以使用两种方法确定垃圾收集器是否已经回收引用对象的指示物:明确调用引用对象的 get() 方法,或隐式地通知引用对象已经排入相关引用序列。本例只使用第一种方法。

注意,只有当引用对象自己可触及时,垃圾收集器才会发现它们并将它们添加到相关的引用序列。否则,它们的回收方式将与其他不可触及的对象一样。这也是我们为什么将所有 NativeImage3 实例添加到静态列表(实际上,所有数据结构都满足条件),以确保它们保持不可触及并当其指示物不可触及时得到处理。毫无疑问,我 们还需要确保在处理时将它们删除。该操作由 dispose() 方法完成。

当明确调用 Image3 实例的 dispose() 方法时,该实例不会进行事后清除,因为没有必要。 dispose() 方法将从静态列表删除 NativeImage3 实例,这样当其相应的 Image3 类变为不可触及时,它也会变为不可触及。并且如前所述,不可触及的引用对象不会添加到它们相应的引用序列中。

相反,在前面使用 finalization 的所有示例中,finalizable 对象变为不可触及时都需要进行 finalization,无论是否明确处理它们的相关本机资源。

JVM 将确保:当垃圾收集器发现某个 Image3 实例不可触及时,它会将其相应的 NativeImage3 实例添加到其相关的引用序列中。然后,我们必须将它从序列中解除并释放其本机资源。为此,可以在一个 "cleanup" 线程中执行以下方法:

        static void drainRefQueueLoop() {

ReferenceQueue<Image3> refQueue =
NativeImage3.referenceQueue();
while (true) {
NativeImage3 nativeImg =
(NativeImage3) refQueue.remove();
nativeImg.dispose();
}
}
 

但是,要在应用程序中引入一个新线程有时并不容易,往往不能如人所愿。在这种情况下,可以使用另一种方法,即在为每个 NativeImage3 实例分配内存之前清空序列。为此,可以调用 drainRefQueueBounded() 方法,该方法使用 NativeImage3 构造函数,这样我们在分配新内存之前将释放本机图像的资源:

static final private int MAX_ITERATIONS = 2;

static void drainRefQueueBounded() {
ReferenceQueue<Image3> refQueue =
NativeImage3.referenceQueue();
int iterations = 0;
NativeImage3 nativeImg = (NativeImage3) refQueue.poll();
while (nativeImg != null && iterations < MAX_ITERATIONS) {
nativeImg.dispose();
++iterations;
nativeImg = (NativeImage3) refQueue.poll();
}
}
 

drainRefQueueLoop()drainRefQueueBounded() 之间的主要区别在于:前者是无穷操作(在序列中的某个新条目可用前 remove() 方法将一直阻塞),而后者执行固定量的工作。如果序列中没有条目,那么 poll() 方法将返回 null,并且该方法将只循环 MAX_ITERATIONS 次。这样即使引用序列很长,也不用花费非常长的时间。

上面的示例相当简单。经验丰富的开发人员还可以确保不同的引用对象与不同的引用序列相关联,以满足自己的对内存释放排序的需要。 drainRefQueueLoop()drainRefQueueBounded() 方法可以清除所有可用的引用序列,并可根据优先顺序解除序列中的对象。

虽然这种资源清除方法比使用 finalization 更复杂,但是它功能更强大,也更加灵活,并且它可以将 finalization 中的不确定因素减少到最低。同时,它非常类似 finalization 在 JVM 中实际实现的方式。推荐将这种方法用于需要大量本机资源的更多清除控制的项目。小心使用 finalization 已足以应对大多数其他项目。

不到万不得已不要使用 Finalization

本文已经简要介绍了 finalization 在 JVM 中的实现方式。然后,通过示例演示可执行 finalize 的对象如何不必要地保留在内存中并概述了针对此类问题的解决方案。最后,文 章讨论了一个使用弱引用的方法替代 finalization,该方法允许我们以更加灵活和可预测的方式执行事后清除。

但是,完全依赖垃圾收集器识别不可触及对象(以确保与他们相关的本机资源[可能造成资源缺乏]可以回收)具有两个缺点:内存通常是大量的,而使用大量内存防止潜在的资源匮乏问题并不是一个好策略。因此,当 您使用占用了本机资源的对象(比如说 GUI 组件、文件或套接字)时,在使用结束时一定要调用它的 dispose() 方法或等价方法。这样将确保立即回收本机资源并减少资源损耗的可能性。因此,本文所讨论的事后清除方法只适合作为最后的手段而不应是主要的清除机制。

同时,finalization 应该在完全必要的情况下才能使用。Finalization 是一个不确定的(并且有时也是不可预测的)过程。对 finalization 的依赖越少,对 JVM 和应用程序的影响也越小。请阅读由 Joshua Bloch 撰写的教程: Effective Java Programming Language Guide第 2 章条目 6:Avoid finalizers。

注意:本文只介绍了使用 finalization 所带来的两种问题。finalization 和 Reference 类的使用还会造成非常微小的同步问题。有关这些问题的详细信息,请参阅 Hans-J. Boehm 的 2005 JavaOne Conference 幻灯片, Finalization, Threads, and the Java Technology-Based Memory Model

* 本站所使用的术语"Java 虚拟机" 或 "JVM" 表示 Java 平台的虚拟机。

更多信息

Joshua Bloch。 Effective Java Programming Language Guide 。Addison-Wesley,2001。

Hans-J. Boehm。 Finalization, Threads, and the Java Technology-Based Memory Model 。技术会议 3281, 2005 JavaOne Conference。

致谢

感谢 Peter Kessler 和 Brian Goetz 为本文所提出的建设性意见。

本文 于 2005 年 12 月 27 日发表在 DevX.com 网站上。

关于作者

Tony Printezis 是 Sun Microsystems 的 Java HotSpot Virtual Machine 开发小组的一员成员。他 的大部分时间用于动态内存管理,主要专注垃圾收集器的可伸缩性、响应能力、并行性和可视性。