Java Reference Types: Weak, Soft, and Phantom References

Java has four reference strengths that control how aggressively the garbage collector reclaims objects. Understanding them lets you build memory-sensitive caches, prevent memory leaks in maps, and perform post-GC cleanup without relying on the deprecated finalize() method.

Contents

The Four Reference Strengths

  • Strong reference: the default (Object o = new Object()). The GC never collects a strongly reachable object.
  • Soft reference (SoftReference<T>): GC collects when memory is low (before throwing OutOfMemoryError). Good for memory-sensitive caches.
  • Weak reference (WeakReference<T>): GC collects at the next collection cycle when only weakly reachable. Good for canonicalizing mappings (e.g., interning).
  • Phantom reference (PhantomReference<T>): GC enqueues it after the object is finalized but before memory is reclaimed. Used for post-GC resource cleanup.
Java
import java.lang.ref.*;

// Strong — GC never collects this
String strong = new String("hello");

// Soft — collected when JVM needs memory
SoftReference<String> soft = new SoftReference<>(new String("cached"));

// Weak — collected at next GC when no strong/soft refs exist
WeakReference<String> weak = new WeakReference<>(new String("weakly-held"));

// Phantom — referent always returns null; used with ReferenceQueue
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantom = new PhantomReference<>(new Object(), queue);

SoftReference: Memory-Sensitive Caches

SoftReference objects are guaranteed to be cleared before the JVM throws OutOfMemoryError. This makes them ideal for caches that should hold data as long as memory permits.

Java
class ImageCache {
    private final Map<String, SoftReference<BufferedImage>> cache = new HashMap<>();

    public BufferedImage get(String path) {
        SoftReference<BufferedImage> ref = cache.get(path);
        if (ref != null) {
            BufferedImage img = ref.get(); // null if GC'd
            if (img != null) return img;   // cache hit
        }
        // Cache miss — load from disk
        BufferedImage img = loadFromDisk(path);
        cache.put(path, new SoftReference<>(img));
        return img;
    }

    public void evictStale() {
        // Remove entries whose referents were GC'd
        cache.values().removeIf(ref -> ref.get() == null);
    }
}
SoftReferences tied to a ReferenceQueue let you receive notification when objects are cleared: new SoftReference<>(obj, queue). Poll the queue to clean up stale map entries.

WeakReference: Canonicalizing Mappings

WeakReference objects are collected at the first GC cycle where the referent is only weakly reachable. They're used for interning/canonicalizing: mapping a key to a canonical object, where you don't want the map to prevent GC of unused objects.

Java
// Simple WeakReference usage
String data = new String("hello");
WeakReference<String> weakRef = new WeakReference<>(data);

System.out.println(weakRef.get()); // "hello"
data = null;                        // remove strong reference
System.gc();                        // suggest GC run
System.out.println(weakRef.get()); // likely null (GC'd)

// Canonicalizing pool — intern objects with weak references
class WeakPool<T> {
    private final Map<T, WeakReference<T>> pool = new HashMap<>();

    public T intern(T value) {
        WeakReference<T> ref = pool.get(value);
        if (ref != null) {
            T existing = ref.get();
            if (existing != null) return existing; // reuse canonical instance
        }
        pool.put(value, new WeakReference<>(value));
        return value;
    }
}

PhantomReference: Post-GC Cleanup

PhantomReference.get() always returns null: you can never access the referent through a phantom reference. Instead, when the GC is about to reclaim the object's memory, it enqueues the phantom reference into the associated ReferenceQueue. You poll the queue to perform cleanup.

Java
class NativeResource {
    // Simulate a native handle
    private long handle;
    NativeResource(long handle) { this.handle = handle; }
}

class NativeResourceCleaner extends PhantomReference<NativeResource> {
    private final long handle; // store what we need for cleanup

    NativeResourceCleaner(NativeResource resource, ReferenceQueue<NativeResource> queue) {
        super(resource, queue); // note: referent NOT stored in phantom ref
        this.handle = resource.handle;
    }

    void clean() {
        System.out.println("Cleaning native handle: " + handle);
        // freeNative(handle);
    }
}

// Usage — background thread polls the queue
ReferenceQueue<NativeResource> queue = new ReferenceQueue<>();
Set<NativeResourceCleaner> cleaners = new HashSet<>(); // keep strong refs to cleaners!

NativeResource res = new NativeResource(42L);
cleaners.add(new NativeResourceCleaner(res, queue));
res = null; // remove strong reference

// Background cleanup thread
Thread.startVirtualThread(() -> {
    while (true) {
        try {
            NativeResourceCleaner cleaner = (NativeResourceCleaner) queue.remove();
            cleaner.clean();
            cleaners.remove(cleaner);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            break;
        }
    }
});
You MUST keep a strong reference to the PhantomReference object itself, if no strong reference exists, the phantom reference itself will be GC'd before it can be enqueued. The Java Cleaner API (Java 9+) handles this bookkeeping for you.

WeakHashMap

WeakHashMap wraps each key in a WeakReference. When a key is GC'd, the entry is automatically removed from the map. This prevents the map from keeping objects alive longer than their natural lifetime.

Java
WeakHashMap<Object, String> map = new WeakHashMap<>();

Object key1 = new Object();
Object key2 = new Object();
map.put(key1, "value1");
map.put(key2, "value2");
System.out.println(map.size()); // 2

key1 = null; // remove strong reference to key1
System.gc();
Thread.sleep(100); // give GC a chance
System.out.println(map.size()); // 1 — key1 entry was removed automatically

// WeakHashMap is useful for:
// - Per-object metadata maps that should not prevent GC
//   (e.g., attaching rendering state to domain objects)
// - Listener registrations that should auto-remove when listener is GC'd
// - Caches where the value is derived from the key

// WARNING: WeakHashMap is NOT thread-safe.
// Use Collections.synchronizedMap(new WeakHashMap<>()) for thread safety.
Don't use a WeakHashMap when the keys are interned Strings, boxed primitives (Integer, Long), or other objects with permanent strong references: their entries will never be removed.