与垃圾收集器交互以避免内存泄漏
本篇文章介绍在基于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
是一个包含JList
的JFrame
子类。创建新的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本身是一个抽象基类,包含三个具体的子类:SoftReference
、WeakReference
和PhantomReference
。
通过引用对象引用的对象称为引用。当您创建一个引用子类的实例时,需要指定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
.侦听器对象,在调用它们的elementAdded
或elementRemoved
方法之前为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
,比如整数。可以想象这样一种情况:这些对象被垃圾收集,而对它们的弱引用仍然存在,等待自己被垃圾回收。这些引用在一起可能会占用大量内存,除非您释放它们,否则您可能会用完它们。然而,这将是一种罕见的情况。
除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/1063.html
暂无评论