W3docs

Java Reference Types: Strong, Weak, Soft, Phantom

How Java reference strengths interact with the garbage collector to enable caches, listeners, and cleanup.

Java Reference Types: Strong, Weak, Soft, Phantom

Garbage collection feels automatic, but Java gives you a dial to influence it. An ordinary variable is a strong reference that pins an object in memory. The java.lang.ref package adds three weaker grades — soft, weak, and phantom — that let the garbage collector reclaim an object even while you still hold a handle to it. These reference types are the foundation of memory-sensitive caches, leak-free listener registries, and reliable resource cleanup.

Reachability decides who lives

The garbage collector keeps an object alive as long as it is reachable from a GC root (a live thread, a static field, a stack variable). The strength of the references on the path to an object determines its reachability class, and that class decides its fate when memory is needed.

Referenceget() returns object?GC keeps it alive?Typical use
StrongAlwaysAlways (while strongly reachable)Ordinary variables and fields
SoftUntil memory runs lowUntil the heap is under pressureMemory-sensitive caches
WeakUntil the next GC clears itNoCanonicalizing maps, listeners
PhantomNever (always null)NoPost-mortem cleanup

The order from strongest to weakest is: strong → soft → weak → phantom. When several references of different strengths point at one object, the strongest one wins — a single strong reference is enough to keep an object alive forever.

Strong references: the default

Every reference you write without the java.lang.ref API is strong. As long as one strong reference is reachable, the object cannot be collected — this is the source of most memory leaks, where a forgotten entry in a long-lived collection keeps objects alive indefinitely.

List<byte[]> cache = new ArrayList<>();
cache.add(new byte[10_000_000]); // 10 MB pinned by a strong reference
// Nothing here can be collected until 'cache' itself becomes unreachable.

Soft references: memory-sensitive caches

A SoftReference lets the collector reclaim the object only when the heap is running low. While memory is plentiful, get() keeps returning the object, which makes soft references the natural fit for caches that should shrink under pressure rather than cause an OutOfMemoryError.

SoftReference<BufferedImage> ref = new SoftReference<>(loadThumbnail(path));

BufferedImage cached = ref.get();
if (cached == null) {             // GC reclaimed it under memory pressure
  cached = loadThumbnail(path);   // recompute and re-wrap
  ref = new SoftReference<>(cached);
}
return cached;

The JVM guarantees that all soft references to softly-reachable objects are cleared before it throws OutOfMemoryError, so a soft-reference cache is the last thing sacrificed, not the first.

Weak references: maps and listeners

A WeakReference does not delay collection at all. The instant an object is only weakly reachable, it becomes eligible for GC and get() will return null after the next collection cycle. This is exactly what you want for keys in a cache that should disappear when no one else uses them — which is what WeakHashMap is built on.

WeakHashMap<Widget, Metadata> sidecar = new WeakHashMap<>();
sidecar.put(widget, metadata);
// When 'widget' is no longer strongly referenced elsewhere, the entry
// vanishes automatically — no manual remove(), no leak.

Pairing a reference with a ReferenceQueue lets you be notified when the referent is collected: the GC enqueues the cleared reference so you can run follow-up logic.

ReferenceQueue<Widget> queue = new ReferenceQueue<>();
WeakReference<Widget> ref = new WeakReference<>(widget, queue);
// later, on a cleanup thread:
Reference<?> dead = queue.remove(); // blocks until a referent is collected

Phantom references: post-mortem cleanup

A PhantomReference is the weakest grade and the most specialized. Its get() always returns null, so you can never resurrect the object through it. Its sole purpose is to be enqueued after the object has been collected, giving you a safe hook to release native resources — the modern, reliable replacement for the deprecated finalize().

ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantom = new PhantomReference<>(resource, queue);
// A background thread drains the queue and frees the off-heap buffer
// only once the JVM confirms the object is truly gone.

The JDK's own java.lang.ref.Cleaner (Java 9+) is built on phantom references and is what you should reach for in real code instead of managing the queue by hand.

A worked example: all four strengths in one run

This program creates one object held only by each kind of reference, forces a garbage collection with System.gc(), and reports what survived. It also wires ReferenceQueues to the weak and phantom references so you can watch the GC notify you of each death.

java— editable, runs on the server

What to take from the run:

  • The strong reference printed the same Resource(kept-alive) at the start and the end. Despite two System.gc() calls in between, a strongly reachable object is never a collection candidate — strong references always win.
  • weak.get() returned Resource(weakly-held) before the GC but null afterward. Once the only strong link (onlyWeaklyHeld) was set to null, the object was merely weakly reachable, so the very next collection cleared the weak reference.
  • weak ref enqueued? : true confirms the GC pushed the cleared WeakReference onto its ReferenceQueue. That enqueue is the notification mechanism — it is how WeakHashMap and listener registries learn that an entry can be purged.
  • soft.get() still returned Resource(cache-entry) after gc(). The heap was under no pressure, so the collector kept the softly-reachable object alive — exactly the behavior that makes soft references suitable for caches that only shrink when memory is tight.
  • phantom.get() printed null even before any collection, yet phantom enqueued? : true shows it was still queued once its referent died. A phantom reference never hands back the object; it exists purely to signal after the fact that cleanup can safely run.

Practice

Practice

You need a cache that holds computed values to speed up your program but should automatically release them rather than cause an OutOfMemoryError when the heap runs low. Which reference type fits best?