Page 3 of 5
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 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.
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.
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.
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!
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.
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.
java.lang.refjava.lang.ref packageListener interface, and to implement the Event class, within VectorModel. I think this is a very appropriate use of top-level, nested classes