apike.ca

Weak and Soft References in Java

By mbystedt on Aug 30, 2018, 5:20:10 PM

All references in Java are strong by default. When discussing Java references, the strong reference is generally all that is introduced. After all, it is the default and, in most situations, is all you'll ever need to use. A strong reference ensures the garbage collector won't free the pointed to object. That's an excellent behavior. Your computer probably shouldn't be conspiring to free objects behind your back.

Java does have two other reference types that allow the garbage collector to more aggressively reclaim heap space (with your permission). This page will examine the differences between the weak and soft reference types and the reasons for allowing the garbage collector to reclaim objects more dynamically.

Java has no keywords or special syntax for creating a weak or soft reference. Instead, the package java.lang.ref provides classes that can turn a strong reference into other types. WeakReference and SoftReference are special classes which wrap a strong reference and convert it to a weak or a soft reference respectively.

Weak References

import java.lang.ref.WeakReference;
// ...
this.weakObjectReference = new WeakReference<SomeClass>(object);
Code 1: Creating a weak reference. The incurs the overhead of creating a second object.

A weak reference is ignored by the garbage collector when deciding if an object should be kept in the heap. An object with a 100 weak references that loses its last strong reference will likely be removed from the heap during the next pass of the garbage collector.

The reason for using a weak reference (or a soft one) is to flag objects that can be reclaimed (when there are also no strong references). Typically, this occurs with data that can be quickly recreated and is only temporarily needed. Consider an application that generates a large data structure to display a graph. The class that generates the data structure to display the graph can use a weak reference to provisionally retain the work. The code controlling the graph's display holds the strong reference. The data structure is then freed and the weak reference is set to null when the application no longer displays the graph.

In short, weak references are best used when there is a need to decline control over keeping the object in memory.

SomeClass object = this.weakObjectReference.get();
if (object != null) {
    // ...
}
Code 2: Retrieving a weak reference.

The WeakReference class acts a wrapper for the object you place in it. If, at some point, the object gets garbage collected then get() will return null. There's no danger of the object expiring after you get the object as this creates a strong reference.

A typical usage of a weak reference is for breaking circular references. A circular reference is when object A has a reference to object B and the reverse is true. So, they both point to each other. If B only has a weak reference to A then the reference cycle is broken. This isn't such a concern with modern Java garbage collection as it also looks for a strong path back to what it considers the root of allocation. So, strong reference loops that have been cast adrift are detected and released. It's still possible for forgotten strong references to create a memory leak as is shown in the following example. This example is often referred to as the "lapsed listener" anti-pattern.

package ca.apike;
 
import java.lang.ref.WeakReference;
 
public class BadController implements DataObjectListener {
 
    private DataObject dataObject;
 
    public void registerDataObject(final DataObject dataObject) {
        this.dataObject = dataObject;
        if (this.dataObject != null) {
            this.dataObject.addListener(this);
            // this.dataObject now has a reference to this
        }
    }
 
    // Called by the DataObject we are listening to
    public void dataObjectEvent(final DataObject dataObject) {
 
    }
 
    public void stopController() {
        if (this.dataObject != null) {
            this.dataObject.removeListener(this);
        }
    }
}
Code 3: We add a listener in registerDataObject() but forget to remove the existing one.

Let's assume dataObject refers to something that should remain in memory. The controller is a temporary object that should leave the heap after stopController() is called. If registerDataObject() is called twice and dataObject doesn't use a weak reference to the badController object, the controller is going to stay in memory. The controller has lost the reference to the first object and it is now stuck in memory.

The bug is that BadController is not removing itself in registerDataObject(). With a strong reference to the controller, the data object has been made into a unwitting accomplice. Weak references to listening controllers would cause no hardship for the data object. It works independently of the controller. In general, it is the case the strong references become a problem when the one holding it is only passively involved with the target object.

package ca.apike;
 
import java.util.WeakHashMap;
 
public class DataObject {
    private WeakHashMap<DataObjectListener, Boolean> changeListeners = 
        new WeakHashMap<DataObjectListener, Boolean>();
 
    public void addListener(final DataObjectListener listener) {
	changeListeners.put(listener, true);
    }
 
    public void removeListener(final DataObjectListener listener) {
	changeListeners.remove(listener, true);
    }
}
Code 4: Correct implementation for the DataObject using a weak reference.

Weak references can also be used for creating "canonicalizing mappings." In other words, you can use it to map something like an account id to an account. If the referenced account object is null then it can be refetched from the database. While your application uses the account object, it will remain nicely cached. While a neat trick, weak references shouldn't be used as a replacement for a real caching policy. This could cause problems where a distributed applications retains account information a long time after another instance saved new account information.

Soft References

Soft references sit somewhere between a weak and a strong reference. A soft referenced object will be garbage collected before memory runs out. The exact timing when a soft reference is reclaimed is up to the virtual machine implementation. This makes a soft reference a simple way (if somewhat unpredictable) to cache data. Of course, even one strong reference saves the object from being turfed from the heap.

Compared to a strong reference, they allows the virtual machine to respond better to memory pressures beyond not being able to allocate memory. A lack of memory will likely crash your application.

Large data structures that can be easily reloaded should consider using soft references. When combined with lazy loading, the user of the class will never notice. Things like audio data and image data that were loaded from disk can easily be reloaded. A data structure that ties up megabytes of memory can slim down as required with barely any effort.

package ca.apike;
 
import java.lang.ref.SoftReference;
// ...
 
public class AudioDataFile {
 
    private SoftReference<ByteBuffer> dataSoftRef;
    public byte[] getData() {
        if (dataSoftRef == null || dataSoftRef.get() == null) {
            // lazy load data...
            this.dataSoftRef = 
                new SoftReference<ByteBuffer>(...);
        }
 
        return dataSoftRef.get().array();
    }
}

A soft reference is more suitable than a weak reference for caching.

Conclusion

Using the often overlooked weak and soft references in Java can prevent memory leaks and improve memory utilization. A reference should be weak if the owning object has no active involvement with the target. A soft reference should be used to flag data that can be removed when it is not actively being used.

References