4年前 (2020-12-20)  jvm |   抢沙发  898 
文章评分 1 次,平均分 4.0

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

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

使用WeakReference来防止内存泄漏

面向对象程序和类库通常使用模型-视图-控制器(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,比如整数。可以想象这样一种情况:这些对象被垃圾收集,而对它们的弱引用仍然存在,等待自己被垃圾回收。这些引用在一起可能会占用大量内存,除非您释放它们,否则您可能会用完它们。然而,这将是一种罕见的情况。
使用WeakReference来防止内存泄漏

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/1063.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册