Java Tip 79: Interact with garbage collector to avoid memory leaks

Use reference objects to prevent memory leaks in applications built on the MVC pattern

Object-oriented programs and class libraries often use the model-view-controller (MVC) design pattern. Swing, for example, uses it extensively. Unfortunately, using MVC in a garbage-collected environment such as Java introduces additional serious problems. Imagine, for instance, that your program uses a data model that exists for the lifetime of your application. A user can create views of that model. When he loses interest in the view, he can dispose of it -- or he'll want to dispose of it, at any rate. Unfortunately, the view is still registered as a listener of the data model and cannot be garbage-collected. Unless you explicitly remove every view from the data model's listeners list, you will get loitering zombie objects. The garbage collector can still reach these objects, even though you will never use them and want the garbage collector to discard them.

This Java tip shows you how to use reference objects, introduced in JDK 1.2, to solve this problem. By interacting with the garbage collector, you can eliminate loiterers and lapsed listeners, terms suggested by Ed Lycklama (see the Resources section below for more details). Lycklama generally defines a loiterer as an object that persists past its usefulness. The loiterer category is further broken down into four patterns; most common is the lapsed listener, an object added to, but never removed from, a collection of listeners.

Example problem

In this article, I'll examine a simple Swing MVC application to illustrate how an application using the MVC pattern creates lapsed listeners and memory leaks. Then, I'll show you how to modify the application in order to remove the memory leaks. The example application has a simple data model that contains some strings. The application's main window, shown in Figure 1, lets the user add new strings to, and create new views of, the data model. Both processes are illustrated in the figure below. The application's main window also shows the number of views that are alive, meaning that they are created, but not yet finalized. Each view is a separate frame containing a Jlist, which displays the strings from the data model (not shown here). A view listens to changes in the data model and updates itself accordingly. You will find the full source for this example in Resources.

Figure 1. The application's main window

First, I am going to define the example data model, but not implement it. Then, I'll implement that model's view. Finally, I'll actually implement the data model in four different ways, showing different implementation trade-offs.

Defining the model

The interface VectorModel defines the data model of the example application:

VectorModel.java

 
package mldemo;
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);
} 

Whenever an element is added to or removed from the vector, the VectorModel's implementations must notify their listeners.

Implementing the view

VectorListFrame, the view of this model, is a JFrame subclass that contains a JList. When a new VectorListFrame object is created, the contents of the VectorModel are copied into the list's DefaultListModel. VectorListFrame has an anonymous inner class that implements the VectorModel.Listener interface. This inner class is registered as a listener to the VectorModel.

Upon the arrival of a vector event, the inner class delegates the appropriate change to the DefaultListModel instance. For tracking purposes, the constructor of VectorListFrame increases the count of living views stored in the public static field nFrames, while the finalize method decreases that counter. The application's main window uses nFrames to display the number of living views.

VectorListFrame.java

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

When you close the frame, its dispose method is called. In this example, you could easily remove the VectorListFrame from the data model's listeners list. All you need to do is keep a reference to the VectorModel. Then you could call the model's removeListener method from within the view's dispose method (see the code that has been commented out for the implementation).

However, things might not be this simple in a real-world application. The view that is listening to the data model might be deeply nested in a containment hierarchy. To remove it from the model's listeners list, the top-level frame would need to keep a reference to both the model and its view. This is a very error-prone business tactic, and makes for ugly, difficult-to-maintain code. If you forget just one model/view pair, you create a lapsed listener and memory will be leaked. You therefore want a data model that removes lapsed listeners automatically.

I will cover four implementations of VectorModel. The first one is the standard model for implementation; this is how view models in Swing are implemented. The other three implementations use weak references to avoid the lapsed listener problem. You can start the demo application four ways to see these implementations: java mldemo.MLDemo, java mldemo.MLDemo wr, java mldemo.MLDemo twr, and java mldemo.MLDemo qwr .

The standard model implementation

The standard way of implementing a model, such as VectorModel, is straightforward. One vector stores the data elements, and another holds the references to the VectorModel.Listener's objects. This is how the class DefaultVectorModel implements VectorModel.

The implementation has a field, called listeners, that is the vector holding the listeners. Adding a listener to the vector, and notifying all listeners, is very easy and straightforward:

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

The drawback of this approach, of course, is that you can easily get lapsed listeners. The model doesn't know when a listener is just loitering -- that is to say, when it's only reachable through its own vector. When a model is the only object that still knows about a listener object, you should be able to release the listener. In other words, you need the data model to be sensitive to its listener's reachability.

Interacting with the garbage collector

The java.lang.ref package lets you interact with the garbage collector. The basic idea is not to reference objects directly, but rather to do so through special reference objects, which are treated specially by the garbage collector. I give a brief introduction to these reference classes in this article; for more detailed information, on garbage collection in particular, see Resources.

The reference subclasses let you reference objects indirectly. Reference itself is an abstract base class with three concrete subclasses: SoftReference, WeakReference, and PhantomReference.

Objects referenced through a reference object are called referents. When you create an instance of one reference subclass, you specify the referent. Then you call the reference object's get method to access the referent. You can also clear a reference -- that is, you can set it to null. Apart from that, the reference is immutable. You cannot, for example, change the referent. Under specific conditions discussed below, the garbage collector can reclaim the referent and clear all references to it, so always test for null when using the get method!

Java defines different levels of object reachability. An object that is reachable through a path that does not involve any reference objects is said to be strongly reachable. These are normal objects that cannot be garbage-collected. The other reachability levels are defined by the Reference subclasses.

Softly reachable objects

An object that is reachable from a path through a SoftReference object is said to be softly reachable. The garbage collector can reclaim a SoftReference's referent at its own discretion; however, the garbage collector is required to clear all soft references before throwing an OutOfMemoryError. This property makes soft references the leading choice when implementing caches.

Weakly reachable objects

An object that is reachable from a path through a WeakReference object is said to be weakly reachable. When the garbage collector determines that an object is weakly reachable, all weak references to it are cleared. At that time or later, the object is finalized and its memory freed. This makes WeakReference perfect for model-listener implementations, which is why it is used in the second, third, and fourth implementations of VectorModel. These implementations will not leak memory!

Phantomly reachable objects

Finally, an object that is not strongly, softly, or weakly reachable, but reachable from a path through a PhantomReference object, is said to be phantomly reachable. You cannot access the PhantomReference's referent through that reference object. A phantomly reachable object remains so until you explicitly clear all references to it. You can, however, wait for the object to become phantomly reachable. At that time, you could do some cleanup, which must be done before the garbage collector releases the object's memory.

It is important to remember that you do not know beforehand when the garbage collector will finalize and free objects that are no longer strongly reachable. With soft references, you have the guarantee that it will free your objects before throwing an OutOfMemoryError. With weak references, the decision is entirely up to the garbage collector, so your code should never rely on the timing of an object's garbage collection. The JDK 1.2 garbage collector seems to consider weak references quite regularly -- regularly enough, in fact, that you should not have memory leaks when you use them. In the case of phantom references, the garbage collector will not release referents unless you explicitly clear the references.

You can find out that the garbage collector has determined that a reference object's referent is not strongly reachable after the fact, however. To do so, register your reference objects with a ReferenceQueue. The garbage collector will put the reference object in that queue after it clears the reference. You can use the queue's poll method to check for any enqueued references, or use the queue's remove method to wait until a reference is enqueued. I will use both approaches in the third and fourth VectorModel implementations.

To illustrate typical garbage collection in my simple example application, I will encourage the garbage collector a bit. The example uses very little memory; if this were not the case, the garbage collector would not collect any garbage. The application has a thread that updates the number of living views. Before doing so, however, this thread calls System.gc to encourage the garbage collector to collect garbage.

That's the theory. To see it in action, however, be sure to use JDK 1.2.2 or greater. JDK 1.2.2 fixed a bug in JDK 1.2.1 that prevented JFrame objects from being finalized and garbage-collected. Sun lists the bug under the bug IDs 4222516 and 4193023.

The first implementation: Using WeakReferences to implement VectorModel

By using WeakReference objects to hold the references to your data model's listeners, you avoid the problem of lapsed listeners. DefaultVectorModel holds direct, strong references to the listeners, which prevents them from being garbage-collected. The new WeakRefVectorModel implementation holds only indirect, weak references to the listeners. When the garbage collector determines that a listener is only weakly reachable, it finalizes the listener, frees its memory, and clears the weak reference to it. In this example, when the user closes the VectorListFrame, the frame is only weakly reachable from the data model, and can therefore be garbage-collected. Et voilĂ !

You can still add a listener to WeakRefVectorModel quite easily. You create a new WeakReference object inside the addListener method, with the listener as its referent. Then you add the reference object to your listener's vector. The client code never sees the WeakReference.

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

Compare this to the standard implementation, shown below. You see that very little extra code was added:

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

When a view is discarded, the garbage collector clears the WeakReference object to the view. When the reference is cleared, you want to remove it from the listener's vector. The question is, when do you do so?

Whenever an event is fired from WeakRefVectorModel, you have to test the referents, the actual VectorModel.Listener objects, for null before you call their elementAdded or elementRemoved methods. Since you're testing anyway, you should also throw out references that have been cleared. The fireElementAdded method is now a bit more complicated. The code in boldface was added to the code of the standard implementation (see above). This code checks the VectorModel.Listener object for null, and, if it is indeed null, removes the reference object from the listener's vector:

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

Of course, fireElementRemoved works the same way. The disadvantage of this approach is that these two methods are now more complicated. If there were more fire<anything> methods, you would also implement them this way, further bloating the code.

The second implementation: Waiting for the garbage collector

You could also wait for the garbage collector to finalize and free a listener, and clear the references to it. ThreadedWRVectorModel is implemented this way. As mentioned earlier, you need a ReferenceQueue for the garbage collector to add the reference objects when they have been cleared. When you add a listener, you must register the WeakReference object with the queue:

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

When the garbage collector has finalized and freed the listener, it puts that listener's reference object in the queue. Therefore, you only have to wait on the queue for a reference object to be enqueued. You can dedicate a thread to this task, which you create in the ThreadedWRVectorModel's constructor:

  // 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();

The queue.remove() call is blocking, so the thread effectively waits until the garbage collector has freed a listener. When remove returns, you can remove the WeakReference to the listener from the listener's vector. Setting wr to null lets the garbage collector free it. If you don't set wr to null, wr holds on to the reference object until it is overwritten by the next call to queue.remove(). Until then, the reference object is not freed. This would not be too damaging, since the reference object is small, but it is poor style.

When do you stop the cleanUp thread? You must do so eventually, because otherwise you leak the memory of this thread and the model as a whole. The problem is that you cannot simply implement ThreadedWRVectorModel.finalize to stop the thread. As an anonymous inner class, the cleanUp thread has an implicit reference to the ThreadedWRVectorModel instance of which it is part. While the thread is living, ThreadedWRVectorModels cannot be finalized and garbage-collected.

To solve this problem, you introduce a terminate method that stops the cleanUp thread:

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

terminate works just fine. If no other reference to a ThreadedWRVectorModel instance exists, the instance can be garbage-collected after terminate has been called. However, this is not a perfect solution; the programmer must remember to call terminate on that model when he or she wants to discard the model. This is almost as error-prone as removing listeners explicitly!

The third way: Polling the ReferenceQueue

Instead of having a cleanUp thread, you could just implement a cleanUp method. This method would poll the reference queue. Polling the queue is not a blocking operation. If a reference is enqueued, it is returned; but if there are no enqueued references, poll returns immediately with a null reference.

The cleanUp method could poll the queue, and if there is a reference object enqueued, cleanUp can remove the reference from the listeners list. This is the way QueuedWRVectorModel is implemented:

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

Note that, for this approach, you add listeners in the same way as you did in ThreadedWRVectorModel. That means that the weak reference to the listener is registered with the queue.

The cleanUp method could be called from the fire<anything> methods. In that case, these methods would not have to deal with removing cleared references. Also, the user of a QueuedWRVectorModel instance could call cleanUp.

Conclusion

Using the java.lang.ref package, you can interact with the garbage collector so that it frees listeners when they become only weakly reachable. This lets you automate the task of removing these listeners from a model's listeners list.

You have seen three ways of using weak references. Which one is best? WeakRefVectorModel has the advantage of removing cleared references when traversing the listeners list. However, if there are many methods traversing the list, this approach bloats the code unnecessarily.

In that case, I might prefer the overhead of a dedicated thread. It has the advantage of concentrating in one place the code that deals with cleared references. But there is a serious problem with the ThreadedWRVectorModel implementation. As shown here, it is not scalable. For each ThreadedWRVectorModel instance, you have one dedicated cleanUp thread, and there is no simple way around this thread explosion. Therefore, I would use the approach of ThreadedWRVectorModel only if I had a single global application data model.

I think the last approach, QueuedWRVectorModel is the best implementation. It concentrates the code that deals with the queue in one place. The cleanUp method can be called by any other QueuedWRVectorModel method with very little overhead. It might be slightly slower than the other two approaches, but that shouldn't be a serious problem. The main advantage of QueuedWRVectorModel is that it is perfectly scalable. Also, it is far easier to maintain, since the cleanup code is concentrated in just one method.

Nothing is ever perfect. There is a theoretical danger of running out of memory when using WeakReferences without a cleanUp thread. Say you have lots of WeakReferences to very small objects, like Integers. One could imagine a situation in which these objects are garbage-collected while the weak references to them persist, waiting to be garbage-collected themselves. These references together might use so much memory that you could run out, unless you freed them. However, this would be a rare situation.

Raimond Reichert just completed his studies in computer science at the Swiss Federal Institute of Technology, where he minored in teaching. He has been working with Java since early 1996, mostly on user interface designs. Among other things, he writes educational software. His diploma thesis is a Java program aimed at giving a playful introduction to programming for novices. See http://educeth.ethz.ch/informatik/interaktiv/kara/kara.html.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more