Press "Enter" to skip to content

使用WeakReference来防止内存泄漏

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

与垃圾收集器交互以避免内存泄漏

 

本篇文章介绍在基于MVC模式构建的应用程序中,如何使用引用对象来防止内存泄漏。

 

 

面向对象程序和类库通常使用模型-视图-控制器(MVC)设计模式。例如,Swing广泛使用它。不幸的是,在Java等垃圾收集环境中使用MVC会带来额外的严重问题。例如,假设您的程序使用的数据模型存在于应用程序的生命周期中。用户可以创建该模型的视图。当他对这个观点失去兴趣时,他可以把它处理掉——或者他想处理掉,无论如何。不幸的是,视图仍然注册为数据模型的侦听器,无法进行垃圾回收。除非从数据模型的监听器列表中显式删除每个视图,否则您将得到游荡的僵尸对象。 垃圾回收器仍然可以访问这些对象,即使您不会使用它们并希望垃圾回收器丢弃它们。

 

这个Java技巧向您展示了如何使用JDK中引入的引用对象来解决这个问题。通过与垃圾收集器交互,您可以消除闲逛者和失效的侦听器,通常将游荡者定义为一个持续超过其用途的对象。loiter类别进一步细分为四种模式;最常见的是失效侦听器,一种添加到侦听器集合中但从未从中移除的对象。

 

内存泄漏示例问题

 

在本文中,我将研究一个简单的Swing MVC应用程序,以说明使用MVC模式的应用程序如何创建失效的侦听器和内存泄漏。然后,我将向您展示如何修改应用程序以消除内存泄漏。示例应用程序有一个包含一些字符串的简单数据模型。应用程序的主窗口(如图1所示)允许用户向数据模型添加新的字符串和创建新的视图。这两个过程如下图所示。应用程序的主窗口还显示了活动视图的数量,这意味着它们已创建,但尚未完成。每个视图都是一个单独的框架,其中包含一个Jlist,它显示数据模型中的字符串(此处未显示)。视图监听数据模型中的更改并相应地更新自身。

 

首先,我将定义示例数据模型,但不是实现它。然后,我将实现该模型的视图。最后,我将以四种不同的方式实现数据模型,展示不同的实现权衡。

 

定义模型

 

VectorModel接口定义了示例应用程序的数据模型:

 

VectorModel.java

 

import java.util.*;
/**
  * Define a simple "application" data model. You can add, remove, and
  * access objects. When you add or remove an object, all
  * registered VectorModel.Listeners
  * will be notified with a VectorModel.Event.
  *
  * @author Raimond Reichert
  */public interface VectorModel {
  public static class Event extends EventObject {
    private Object element;
    public Event (VectorModel model, Object element) {
      super (model);
      this.element = element;
    } 
    public Object getElement() {
      return element;
    } 
  } 
  public interface Listener extends EventListener {
    public void elementAdded (VectorModel.Event e);
    public void elementRemoved (VectorModel.Event e);
  } 
  public void addElement (Object object);
  public void removeElement (Object object);
  public Object elementAt (int index);
  public int size();
  public void addListener (VectorModel.Listener l);
  public void removeListener (VectorModel.Listener l);
}

 

每当在向量中添加或移除元素时, VectorModel 的实现必须通知它们的侦听器。

 

实现视图

 

这个模型的视图 VectorListFrame 是一个包含 JListJFrame 子类。创建新的 VectorListFrame 对象时, VectorModel 的内容将复制到列表的 DefaultListModel 中。 VectorListFrame 有一个匿名内部类,它实现 VectorModel 侦听器接口。这个内部类注册为 VectorModel 的侦听器。

 

在向量事件到达时,内部类将适当的更改委托给 DefaultListModel 实例。出于跟踪目的, VectorListFrame 的构造函数增加了存储在公共静态字段 nFrames 中的活动视图的数量,而 finalize 方法则减少了该计数器。应用程序的主窗口使用 nFrames 来显示实时视图的数量。

 

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
/**
  * Displays a VectorModel in a small frame, using a
  * JList. Uses a private, anonymous inner class to
  * implement VectorModel.Listener. This inner class
  * adds or removes elements from the JList's data
  * model.
  *
Note: As the code's out-commented lines show, in the case
  * of 
VectorListFrame
, it would be quite easy to
  * remove the object from the 
VectorModel
's listeners list
  * when the frame is closed. Alas, in real-world code, it's not always
  * this easy...
  *
  * @author Raimond Reichert
  */public class VectorListFrame extends JFrame {
  // number of non-finalized VectorListFrames
  public static int nFrames = 0; 
// Commenting out discussed below...
//  private VectorModel vectorModel;
  protected DefaultListModel listModel;
  protected VectorModel.Listener 
    modelListener = new VectorModel.Listener() {
    public void elementAdded (VectorModel.Event e) {
      listModel.addElement (e.getElement());
    } 
    public void elementRemoved (VectorModel.Event e) {
      listModel.removeElement (e.getElement());
    } 
  };
  public VectorListFrame (VectorModel vectorModel) {
    super ("Listing...");
    setSize (200, 200);
    setDefaultCloseOperation (WindowConstants.DISPOSE_ON_CLOSE);
    // In a multi-threaded environment (like Java) you
    // must synchronize the increment and decrement
    // operations on "nFrames."  You can't synchronize
    // on the object being constructed, but must have all
    // constructors synchronize on the same object.  The
    // java.lang.Class object for VectorListFrame is
    // a good candidate for this synchronization.
    synchronized (VectorListFrame.class) {
        nFrames++;
    }
    listModel = new DefaultListModel();
    int size = vectorModel.size();
    for (int i = 0; i < size; i++)
      listModel.addElement (vectorModel.elementAt(i));
    getContentPane().add (new JScrollPane (new JList (listModel)));
    vectorModel.addListener (modelListener);
    // Commenting out discussed below...
//  this.vectorModel = vectorModel;
  } 
/*  Commenting out discussed below...
    public void dispose() {
      super.dispose();
      vectorModel.removeListener (modelListener);
    }
*/  protected void finalize() throws Throwable {
    super.finalize();
    synchronized (VectorListFrame.class) {
        nFrames--;
    }
  } 
}

 

关闭框架时,将调用其 dispose 方法。在这个例子中,您可以很容易地从数据模型的监听器列表中删除 VectorListFrame 。你需要做的就是保持对向量模型的引用。然后,您可以从视图的 dispose 方法中调用模型的 removeListener 方法。

 

然而,在实际应用程序中,事情可能并不是那幺简单。侦听数据模型的视图可能深深嵌套在包含层次结构中。要将其从模型的侦听器列表中删除,顶级框架需要保留对模型及其视图的引用。这是一种非常容易出错的业务策略,并且会导致难看、难以维护的代码。如果您忘记了一个模型/视图对,那幺您将创建一个失效的侦听器,内存将被泄漏。因此,您需要一个自动删除失效侦听器的数据模型。

 

我将介绍 VectorModel 的四个实现。第一个是实现的标准模型;这就是Swing中视图模型的实现方式。另外三个实现使用 weak references 弱引用 来避免失效的侦听器问题。您可以通过四种方式启动演示应用程序来查看这些实现: java mldemo.MLDemo, java mldemo.MLDemo wr, java mldemo.MLDemo twr, and java mldemo.MLDemo qwr

 

标准模型实现

 

实现模型(如 VectorModel )的标准方法很简单。一个向量存储数据元素,另一个向量保存对 VectorModel 侦听器的对象。这就是 DefaultVectorModel 类实现 VectorModel 的方式。

 

实现有一个名为listeners的字段,它是保存侦听器的向量。将侦听器添加到向量并通知所有侦听器非常简单和直接:

 

// in DefaultVectorModel.java (see Resources)
  private Vector listeners;
  // ...
  public void addListener (VectorModel.Listener l) {
    listeners.addElement (l);
  }
  // ...
  protected void fireElementAdded (Object object) {
    VectorModel.Event e = null;
    int size = listeners.size();
    for (int i = 0; i < size; i++) {
      if (e == null) // lazily create event
        e = new VectorModel.Event (this, object);
      ((VectorModel.Listener)listeners.elementAt(i)).elementAdded (e);
    }
  }

 

当然,这种方法的缺点是,你很容易失去 listeners 。这个模型不知道听众什幺时候只是闲逛——也就是说,当侦听器只能通过自己的媒介才能到达。当模型是唯一仍然知道侦听器对象的对象时,您应该能够释放侦听器。换句话说,您需要数据模型对其侦听器的可达性敏感。

 

与垃圾收集器交互

 

这个 java.lang.ref 包允许您与垃圾回收器交互。其基本思想不是直接引用对象,而是通过特殊的引用对象进行引用,这些对象由垃圾回收器专门处理。在本文中,我对这些引用类做了简要介绍。

 

引用子类允许您间接引用对象。Reference本身是一个抽象基类,包含三个具体的子类: SoftReferenceWeakReferencePhantomReference

 

通过引用对象引用的对象称为引用。当您创建一个引用子类的实例时,需要指定referent。然后调用引用对象的get方法来访问referent。您还可以清除引用—也就是说,可以将其设置为null。除此之外,引用是不可变的。例如,您不能更改参照物。在下面讨论的特定条件下,垃圾回收器可以回收referent并清除对它的所有引用,因此在使用get方法时始终测试null!

 

Java定义了不同级别的对象可达性。通过不涉及任何引用对象的路径可到达的对象称为强可达对象。这些是普通的对象,不能被垃圾回收。其他可达级别由引用子类定义。

 

Softly reachable 软可达物体

 

通过软引用对象的路径可到达的对象称为软可及对象。垃圾回收器可以自行决定回收软引用的referent;但是,垃圾回收器需要在抛出 OutOfMemoryError 之前清除所有软引用。此属性使软引用成为实现缓存时的主要选择。

 

Weakly reachable 弱可达对象

 

通过 WeakReference 对象的路径可以到达的对象称为弱可达对象。当垃圾回收器确定某个对象是弱可访问的时,对该对象的所有弱引用都将被清除。在那个时候或之后,对象完成并释放内存。这使得 WeakReference 非常适合模型侦听器实现,这就是为什幺在 VectorModel 的第二、第三和第四个实现中使用它。 这些实现不会泄漏内存!

 

Phantomly reachable 虚可达物体

 

最后,如果一个对象不是强的、软的或弱可及的,但可以通过一个 PhantomReference 对象的路径到达,则称为幻象/虚可及。不能通过该引用对象访问 PhantomReference 的referent。在显式清除对它的所有引用之前,一个幻象可访问的对象保持不变。但是,您可以等待对象变为幻象可及。这时,您可以进行一些清理,这必须在垃圾回收器释放对象内存之前完成。

 

请记住,您事先不知道垃圾回收器何时完成并释放不再强可访问的对象,这一点很重要。使用软引用,您可以保证在抛出 OutOfMemoryError 之前它将释放对象。对于弱引用,决定完全取决于垃圾收集器,因此代码永远不应依赖于对象垃圾回收的时间。JDK垃圾回收器似乎很有规律地考虑弱引用——事实上,非常有规律,以至于在使用它们时不应该有内存泄漏。对于幻象引用,除非显式清除引用,否则垃圾回收器不会释放引用。

 

但是你可以发现,垃圾回收器已经确定引用对象的referent在事实发生之后不是强可访问的。为此,请使用 ReferenceQueue 注册引用对象。垃圾回收器将在清除引用后将引用对象放入该队列中。您可以使用队列的 poll 方法来检查是否有任何已排队的引用,或者使用队列的 remove 方法等待某个引用加入队列。我将在第三和第四个 VectorModel 实现中使用这两种方法。

 

为了在我的简单示例应用程序中演示典型的垃圾回收,我将稍微鼓励一下垃圾收集器。这个例子使用的内存非常少;如果不是这样,垃圾回收器就不会收集任何垃圾。应用程序有一个线程,可以更新活动视图的数量。但是,在执行此操作之前,此线程调用 System.gc 鼓励垃圾收集者收集垃圾。

 

这就是理论。但是,要查看它的实际操作,请确保使用jdk1.2.2或更高版本。jdk1.2.2修复了jdk1.2.1中阻止JFrame对象完成和垃圾收集的错误。Sun在bug id 4222516和4193023下列出了该bug。

 

使用WeakReferences实现VectorModel

 

通过使用 WeakReference 对象保存对数据模型侦听器的引用,可以避免侦听器失效的问题。 DefaultVectorModel 保存对侦听器的直接、强引用,从而防止它们被垃圾回收。新的 WeakRefVectorModel 实现只包含对侦听器的间接、弱引用。当垃圾回收器确定侦听器只能弱访问时,它将完成侦听器,释放其内存,并清除对它的弱引用。在本例中,当用户关闭 VectorListFrame 时,该帧只能从数据模型弱访问,因此可以被垃圾回收。等等!

 

您仍然可以很容易地将侦听器添加到 weakrefectorModel 。您可以在 addListener 方法内创建一个新的 WeakReference 对象,将侦听器作为其引用。然后将引用对象添加到侦听器的向量中。客户端代码永远看不到 WeakReference

 

// in WeakRefVectorModel.java: 
  public void addListener (VectorModel.Listener l) {
    WeakReference wr = new WeakReference (l);
    listeners.addElement (wr);
  }

 

将其与标准实现进行比较,如下所示。您可以看到添加了很少的额外代码:

 

// in DefaultVectorModel.java
  public void addListener (VectorModel.Listener l) {
    listeners.addElement (l);
  }

 

当视图被丢弃时,垃圾回收器会将 WeakReference 对象清除到该视图。当引用被清除时,您需要将其从侦听器的向量中移除。问题是,你什幺时候做?

 

无论何时从 WeakRefVectorModel 激发事件,都必须测试引用,即 VectorModel .侦听器对象,在调用它们的 elementAddedelementRemoved 方法之前为 null 。既然你无论如何都在测试,你也应该扔掉已经被清除的引用。 fireElementAdded 方法现在有点复杂了。黑体代码被添加到标准实现的代码中(见上文)。此代码检查 VectorModel.Listener 对象,如果确实为 null ,则从侦听器的向量中移除引用对象:

 

// in WeakRefVectorModel.java: 
  protected void fireElementAdded (Object object) {
    VectorModel.Event e = null;
    int size = listeners.size();
    int i = 0;
    while (i < size) {
      WeakReference wr = (WeakReference)listeners.elementAt(i);
      VectorModel.Listener vml = (VectorModel.Listener)wr.get();
      if (vml == null) {
        listeners.removeElement (wr);
        size--;
      }
      else  {
        if (e == null) // lazily create event
          e = new VectorModel.Event (this, object);
        vml.elementAdded (e);
        i++;
      }
    }
  }

 

当然, firelementremoved 的工作原理是一样的。这种方法的缺点是这两种方法现在比较复杂。如果有更多的 fire<anything> 方法,您也可以这样实现它们,从而进一步膨胀代码。

 

等待垃圾回收器

 

您还可以等待垃圾回收器完成并释放侦听器,然后清除对它的引用。 ThreadedWRVectorModel 是这样实现的。如前所述,垃圾回收器需要一个 ReferenceQueue ,以便在引用对象被清除后添加它们。添加侦听器时,必须向队列注册 WeakReference 对象:

 

// in ThreadedWRVectorModel.java:  
  //...
  private ReferenceQueue queue;  
  private Thread cleanUpThread;
  public ThreadedWRVectorModel() {
    listeners = new Vector();
    queue     = new ReferenceQueue();
    //...
  }
  public void addListener (VectorModel.Listener l) {
    WeakReference wr = new WeakReference (l, queue);
    listeners.addElement (wr);
  }

 

当垃圾回收器完成并释放侦听器后,它将侦听器的引用对象放入队列中。因此,您只需在队列中等待引用对象进入队列。您可以将一个线程专用于此任务,该任务是在 ThreadedWRVectorModel 的构造函数中创建的:

 

// in ThreadedWRVectorModel.java:  
    Runnable cleanUp = new Runnable() {
      public void run() {
        Thread thisThread = Thread.currentThread();
        WeakReference wr;
        while (thisThread == cleanUpThread) {
          try {
            wr = (WeakReference)queue.remove();
            listeners.removeElement (wr);
            wr = null;
          }
          catch (InterruptedException e) { }
        }
      }
    };
    cleanUpThread = new Thread (cleanUp);
    cleanUpThread.start();

 

这个 queue.remove() 调用阻塞,因此线程有效地等待垃圾回收器释放侦听器。当 remove 返回时,可以从侦听器的向量中删除对侦听器的 WeakReference 。将wr设置为null可以让垃圾回收器释放它。如果不将wr设置为null,wr将保留引用对象,直到下一次调用 queue.remove() 在此之前,引用对象不会被释放。这不会有太大的破坏性,因为引用对象很小,但是样式很差。

 

你什幺时候停止清理线程?你必须最终这样做,否则你会泄露这个线程和整个模型的内存。问题是你不能简单地实现 TThreadedWrveCtorModel.finalize 停止线程。作为一个匿名的内部类,清理线程对它所属的 ThreadedWRVectorModel 实例有一个隐式引用。当线程处于活动状态时,无法完成 ThreadedWrVectorModel 并进行垃圾回收。

 

要解决此问题,需要引入一个终止方法来停止清理线程:

 

// in ThreadedWRVectorModel.java:  
  public void terminate() {
    if (cleanUpThread != null) {
      Thread moribound = cleanUpThread;
      cleanUpThread = null;
      moribound.interrupt();
    }
  } // terminate //

 

终止工作很好。如果其他实例已被回收,则无法在 ThreadModel 引用之后调用其他实例。然而,这并不是一个完美的解决方案;程序员必须记住,当他或她想放弃模型时,在该模型上调用 terminate 。这几乎和显式删除侦听器一样容易出错!

 

轮询ReferenceQueue

 

您可以实现一个清理方法,而不是使用清理线程。此方法将轮询引用队列。轮询队列不是阻塞操作。如果引用已排队,则返回该引用;但如果没有已排队的引用, poll 将立即返回空引用。

 

cleanUp 方法可以轮询队列,如果有引用对象进入队列, cleanUp 可以从监听器列表中删除引用。这是 QueuedServiceModel 的实现方式:

 

// in QueuedWRVectorModel.java: 
  public void cleanUp() {
    WeakReference wr = (WeakReference)queue.poll();
    while (wr != null) {
      listeners.removeElement (wr);
      wr = (WeakReference)queue.poll();
    }
  } // cleanUp //

 

注意,对于这种方法,添加侦听器的方式与在 ThreadedWRVectorModel 中的方法相同。这意味着对侦听器的弱引用已注册到队列中。

 

cleanUp 方法可以从 fire<anything> 方法调用。在这种情况下,这些方法就不必处理删除已清除的引用。另外, QueuedServiceModel 实例的用户可以调用 cleanUp

 

结论

 

使用 java.lang.ref 包,则可以与垃圾回收器交互,以便在侦听器变得只能弱访问时释放侦听器。这使您可以自动执行从模型的侦听器列表中删除这些侦听器的任务。

 

您已经看到了使用弱引用的三种方法。哪一个最好? WeakRefVectorModel 的优点是在遍历侦听器列表时删除已清除的引用。但是,如果有许多方法遍历列表,这种方法会不必要地膨胀代码。

 

在这种情况下,我可能更喜欢使用专用线程的开销。它的优点是将处理清除引用的代码集中在一个地方。但是 ThreadedWRVectorModel 实现有一个严重的问题。它是不可伸缩的。对于每个 ThreadedWRVectorModel 实例,您都有一个专用的清理线程,并且没有简单的方法可以避免线程爆炸。因此,只有当我有一个全局应用程序数据模型时,我才会使用 ThreadedWRVectorModel 方法。

 

我认为最后一种方法, queuedwervectorModel 是最好的实现。它将处理队列的代码集中在一个地方。 cleanUp 方法可以由任何其他 QueuedServiceModel 方法调用,开销非常小。它可能比其他两种方法稍慢,但这应该不是一个严重的问题。 queuedwervectorModel 的主要优点是它是完全可伸缩的。而且,由于清理代码只集中在一个方法中,所以维护起来要容易得多。

 

没有什幺是完美的。在没有清理线程的情况下使用 weakreference 时,理论上存在内存不足的危险。假设你对非常小的对象有很多 weakreference ,比如整数。可以想象这样一种情况:这些对象被垃圾收集,而对它们的弱引用仍然存在,等待自己被垃圾回收。这些引用在一起可能会占用大量内存,除非您释放它们,否则您可能会用完它们。然而,这将是一种罕见的情况。

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注