[翻译]如何解决java finalize方法造成的内存遗留问题

2018-01-04  本文已影响67人  徐士林

Finalization允许你在java对象上做一些内存清理的事情,即使不明确的使用这个方法,它也会造成内存回收的延迟。

Finalization是java语言的一个特性,它允许你在GC收集器发现对象不可达后在该对象上做一些内存清理的工作。通常用于回收与对象关联的本地资源。如下是一个简单的例子

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;
}

Image1的实例不可达之后,JVM会调用实例的finalize()方法来确保保存图像数据的本地资源(本例中是由nativeImg指向的数据)已被回收。

但是请注意,尽管被JVM特殊处理,finalize()方法是一个包含任意代码的普通方法。而且可以访问对象中的任何字段(如本例中的pos和dim),更神奇的是该方法可以通过静态实例(如,randomImg=this)使对象变得重新可达。我真的不推荐这种复活对象的编程习惯,但是很遗憾,java语言允许这样做。


finalizable对象的生存周期
  1. 当分配对象时,JVM内部会记录这个对象是finalizable(这通常会减慢现代JVM具有的快速的对象分配路径
  2. 当垃圾收集器发现obj是不可达的,并且注意到这个obj是finalizable(之前分配对象的时候有记录),JVM会把这个obj加入到finalization queue。同时确保从这个obj出发所有能访问到的对象都会被保留,即使那些对象已经是不可达的。 Image1的实例对象
  3. 稍后,JVM的finalizer线程会将obj排出队列,并调用obj的finalize()方法,同时记录obj的finalizer方法已经被调用,这是obj可以认为是finalized
  4. 当垃圾收集器重新发现obj是不可达的,收集器会回收obj并且所有从obj可达的对象(该对象无法通过其他方式可达)

注意,垃圾收集器需要最少两轮收集才能彻底清理obj,并且期间还要保留所有从obj可达的对象。如果程序员不关系这个短暂的不可预料的资源保留(本来应该被清理的资源)。另外,JVM不能保证一定会调用已分配finalizable对象的finaliza()方法,程序可能在发现这些对象不可达之前就退出了。

子类化时避免内存保留问题
FInalization特性使得即使没有显式的使用finalize方法,也会造成资源回收的延迟。考虑如下的例子

public class RGBImage1 extends Image1 {
    private byte rgbData[];
}
RGBImage1 继承自Image1并且加入了新的字段(或者还有一些没有展示的方法)。即使没有在RGBImage1中显式定义finalize()方法,这个子类也会自动冲父类Image1中继承finalize方法。所以所有的RGBImage1的实例都会认为是finalizable。一个RGBImage1的实例不可达之后,实例拥有的可能非常达的rgbData数组在实例被认为是finalized之前都不会被回收。 子类化

这种现象是非常难发现的,因为finalizer可能在隐藏在非常深的父类层级中。

解决这个问题的一个办法是改变编码的方式,使子类拥有引用“父类”的字段,而不是继承父类。

public class RGBImage2 {
    private Image1 img;
    private byte rgbData[];

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

相比于RGBImage1,RGBImage2包含一个Image1的实例而不是继承Image1。当RGBImage2的实例变得不可达时,只有Image1的实例会进入finalization queue,垃圾回收器会及时回收RGBImage2实例以及rgbData数组(假设该数组不会在其他地方被引用)


RGBImage2实例会及时的被回收

由于RGBImage2类不会继承Image1,所以它不会继承任何方法。因此,您可能必须在RGBImage2中添加特定的方法以便实现对Image1中方法的调用。(dispose()方法就是这样一个例子)。

但是不能一直像上面那样改写自己的代码。有时候,作为类的使用者,不得不做更多的工作确保在其实例被认为是finalized的时候不会占据过多的空间,下面的代码是一个实现方案。

public class RGBImage3 extends Image1 {
    private byte rgbData[];

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

RGBImage3和RGBImage1差不多,但是重写了dispose()方法,在这个方法里,将字段rgbData置为null值。你可以显式的调用dispose()方法确保及时rgbData。仅仅推荐在少数场合下会把字段设置为null值。这里是一个例子


5.gif

在内存保留的问题上屏蔽使用者

之前叙述了在第三方类是finalizer性质的,我们调用第三方类时避免内存驻留问题的解决方案。本节介绍如何编写需要事后清理的类,以便用户不会遇到前面概述的问题。最好的办法是将这样的类分成两个类(一个保存finalize过程需要清理的数据,另一个保存所有其他的数据),并且只在前者上定义finalize方法。以下代码说明了这种技术:

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相似,但是包含了一个引用另一个类(NativeImage2)的字段,所有从image类访问nativeImg必须经过一个间接的隔离。因此,当Image2的实例不可达后,只有NativeImage2实例会进入道finalization queue,从Image2实例访问的任何资源都会被回收。

一个微妙的地方是,NativeImage2不应该是Image2的内部类。内部类的实例有一个外部类的隐式引用。因此,如果NativeImage2是Image2的内部类,并且NativeImage2实例进入队列等待finalization,该实例仍然会保留相应的Image2实例,这正是我们想要避免的。但是NativeImage2类只能从Image2类的内部访问,这也是,NativeImage2没有public方法的原因。

Finalization的一个替代方案

上面讨论的例子仍有一个不确定的因素:JVM不能保证对finalization queue中的对象的finalize()方法的调用顺序,所有类的finalizers都会被公平调用。因此,持有大量内存或稀有本地资源的对象可能会滞留在队列中,而占用资源较少的对象会先被处理。

为了避免这种类型的非确定性,可以使用弱引用而不是finalization作为资源清理的方案。使用这种方式,使用者会拥有处理本地资源的能力,而不是依赖JVM去清理资源。下面的例子使用了这样的解决方案

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() {
        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相似,但是其清理工作依赖于弱引用,而不是加入Finalization性质。NativeImage3继承了WeakReference,并且弱引用Image3实例。请记住,当引用对象(在本例中为WeakReference)的引用变得无法访问时,引用对象将被添加到与之相关联的引用队列中。将nativeImg嵌入到引用对象本身中,可以确保JVM正确地排入需要的内容。 将nativeImage嵌入到引用对象内部

可以通过两种方式确定引用对象的引用是否已被垃圾回收器回收:通过调用引用对象上的get()方法,或者通过观察引用对象是否已经在其关联的引用队列中。这个例子只使用后者。

请注意,引用对象仅有垃圾回收器发现,并且在对象只对自己可达时才会进入相关联的引用队列。否则,他们就像其他任何不可达的对象一样被回收。这就是为什么将所有NativeImage3实例添加到静态列表(实际上,任何数据结构就满足):确保他们的引用对象不可达之后,他们仍是可达的并且可以被处理的。当然,您也必须确保在处置它们时将它们从列表中删除(这是在dispose()方法中完成的)。

当在Image3实例上显式调用dispose()方法时,将不会在该实例上进行事后资源的清理;dispose()方法从静态列表中删除NativeImage3实例,以便Image3实例变得不可达时,NativeImage3实例也是不可达的。而且,如前所述,不可达的引用对象是不会被添加到相应的应用队列中。相反,前面所有使用finalization的例子,当对象变为不可达时,都会被考虑成finalization进入队列,而不关心你是否显式的释放了其相关的本地资源。

VM将确保在发现垃圾收集器无法访问Image3实例时,将其对应的NativeImage3实例添加到其关联的引用队列中。然后由使用者NativeImage3实例取出并做资源处理相关工作。这可以通过像下面这样的循环来完成,例如在“清理”线程上执行:

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

这是一个简单的例子。熟练的开发人员还可以确保不同的引用对象与不同的引用队列相关联,根据他们需要确定处理顺序。一个“清理”线程可以根据需要的优先级轮询所有可用的引用队列和出列对象。通常情况下,您可以选择分散资源回收,以减少对应用程序的干扰。

尽管以这种方式清理资源显然是一个比使用finalization更为复杂的过程,但它也更加强大和更加灵活,并且最大限度地减少了与使用finalization相关的大量非确定性。这也与JVM中finalization的方式非常相似。对于明确使用大量本地资源的项目,我建议使用这种方法,这样在清理时也有更多的控制权。大多数其他项目小心使用finalization。

除非是必要的情况下,否则不要使用Finalization

这篇文章简要介绍了JVM中如何实现finalization。然后举例说明了finalization对象在不可达后的内存遗留问题,并简述了解决这个问题的方案。然后介绍了用弱引用代替finalization的方案,这是一个更灵活和可预测的方式清理不可达对象中的资源。

但是,完全依赖垃圾收集器来识别不可达对象,以便可以回收与其关联的本地资源和潜在稀缺资源,这是一个严重的缺陷:内存通常是足够的,并且保留丰富的潜在稀缺资源并不是一个好策略。所以,当你使用完一个关联到本地资源的对象时(例如一个GUI组件,文件,套接字),要调用它的dispose()或者等价的方法来释放资源。这样可以及时的回收资源以及避免资源泄漏。

你也应该试着限制对finalization的使用,只有在绝对必要的时候。finalization的处理过程是一个非确定性的,有时是不可预测的过程。你依赖的越少,它对JVM和你的应用程序的影响就越小。

上一篇下一篇

猜你喜欢

热点阅读