skip to content

Explain the reachability ladder in java.lang.ref: strong, soft, weak, and phantom references — and when you'd use each.

level: seniorimportance: should knowfreq 64%

answer

  1. Ladder: strong > soft > weak > phantom
  2. Soft = clear under memory pressure (caches); cleared before OOM
  3. Weak = clear at next GC when only weakly reachable (WeakHashMap)
  4. Phantom = get() always null, enqueued after finalize → cleanup
  5. All pair with a ReferenceQueue for notification

basics

~20 s

A normal (strong) reference keeps an object alive. SoftReference lets the GC drop the object only when memory is low (good for caches). WeakReference lets it be collected as soon as nothing strong points to it (good for canonicalizing maps). PhantomReference signals after collection, for cleanup.

solid answer

~50 s

java.lang.ref defines a ladder of weaker reachability levels above plain (strong) references. A strong reference is an ordinary field/variable; while one exists, the object is never collected. A SoftReference is cleared at the GC's discretion only under memory pressure, so it's suited to memory-sensitive caches. A WeakReference is cleared eagerly as soon as the object is only weakly reachable (no strong refs), ideal for canonicalizing or metadata maps — WeakHashMap is built on it. A PhantomReference is never returned by get() (always null) and is enqueued only after the object has become phantom-reachable and been finalized; it's the modern, safe hook for post-mortem cleanup of native resources, replacing finalize(). Each can be paired with a ReferenceQueue so you get notified when the referent is cleared. The GC determines reachability strength by the strongest reference chain from any GC root.

code

java · 18 lines
java
import java.lang.ref.*;
import java.util.*;

// Weak: entry disappears once the key is no longer strongly reachable
Map<Connection, Metadata> meta = new WeakHashMap<>();

// Soft: memory-sensitive cache value
SoftReference<byte[]> cached = new SoftReference<>(loadBlob());
byte[] blob = cached.get();        // null if GC reclaimed it
if (blob == null) blob = loadBlob();

// Phantom: post-collection cleanup signal (get() is always null)
ReferenceQueue<Object> q = new ReferenceQueue<>();
Object resource = new Object();
PhantomReference<Object> phantom = new PhantomReference<>(resource, q);
// later, on a cleanup thread:
//   Reference<?> r = q.remove();   // blocks until 'resource' is reclaimable
//   freeNativeBuffer();            // r.get() would be null here

go deeper

for a junior

Knows a normal reference keeps an object alive and that weak/soft references let the GC reclaim it; can name WeakHashMap as a use case.

for a middle

Distinguishes soft (memory-pressure) from weak (next GC) and knows phantom is for cleanup; can pick the right one for a cache vs a metadata map.

for a senior

Explains the reachability ladder precisely, ReferenceQueue mechanics, why phantom.get() is null, and why bounded caches usually beat SoftReference.

for a principal

Reasons about reference-processing cost during GC, queue-draining ownership, interaction with Cleaner/native resources, and when reference types are the wrong tool versus explicit lifecycle management.

## Why weaker references exist By default, every reference you hold is a **strong reference** — an ordinary variable or field. As long as a strong reference is reachable from a **GC root**, the object it points to **cannot be collected**. That's usually what you want, but sometimes you want to say *"keep this only if it's cheap / convenient, otherwise feel free to reclaim it."* The `java.lang.ref` package gives you three weaker levels, forming a **reachability ladder**. An object's **reachability** is defined by the **strongest** kind of reference on any path from a root to it: - **Strongly reachable** — at least one chain of all-strong references reaches it. Never collected. - **Softly reachable** — not strongly reachable, but reachable through a `SoftReference`. - **Weakly reachable** — not strongly/softly reachable, but reachable through a `WeakReference`. - **Phantom reachable** — not reachable by the above, has been finalized, and is reachable only through a `PhantomReference`. - **Unreachable** — nothing reaches it; reclaimable. ## The three reference classes ### `SoftReference<T>` — "keep until memory is tight" The GC is **guaranteed to clear all soft references before throwing `OutOfMemoryError`**, but otherwise *may* keep them as long as it likes. So softly-reachable objects survive normal collections and are dropped only under **memory pressure**. Use case: a **memory-sensitive cache** where re-creating an entry is cheaper than crashing. Caveat: behavior is GC-dependent and hard to tune; a bounded cache (e.g. Caffeine/LRU) is usually a better choice in practice. ```java SoftReference<BigThing> ref = new SoftReference<>(loadBigThing()); BigThing t = ref.get(); // may be null if GC reclaimed it under pressure if (t == null) t = loadBigThing(); ``` ### `WeakReference<T>` — "keep only while someone else holds it strongly" A weakly-reachable object is **collected at the next GC** once no strong (or soft) reference remains. `get()` returns the referent or `null` once cleared. Use case: **canonicalizing maps / associating metadata with keys without preventing their collection**. `WeakHashMap` uses weak *keys*: an entry vanishes automatically once its key is no longer strongly referenced elsewhere — perfect for caches keyed by objects whose lifecycle you don't own. ### `PhantomReference<T>` — "tell me *after* it's gone, so I can clean up" `get()` on a phantom reference **always returns `null`** — you can never resurrect the object. Its only purpose is to be **enqueued into a `ReferenceQueue` after the referent has been finalized and is about to be reclaimed**, giving you a reliable, ordered signal to run cleanup (e.g. free a native buffer, close an off-heap handle). It is the **safe replacement for `finalize()`** (which is deprecated for removal): finalizers can resurrect objects, run on an unspecified thread, and delay collection; phantom-reference cleanup avoids all that. ## ReferenceQueue — getting notified Any of these can be constructed with a **`ReferenceQueue`**. When the GC clears the reference, it **enqueues** the `Reference` object onto that queue (for phantom refs, after finalization). A background thread `poll()`/`remove()`s the queue and performs cleanup. This is the machinery `Cleaner` wraps for you. ## How they relate to the rest of this topic - They are pure **heap + reachability** concepts: the GC's mark phase determines reachability strength from the **GC roots**, then clears soft/weak/phantom refs per the rules above. - `Cleaner` (covered separately) is a higher-level API built on **phantom references + a reference queue + a worker thread** — the modern finalization replacement. - Misusing them is a leak/footgun source: holding a strong reference *and* a weak one keeps the object alive; forgetting to drain a `ReferenceQueue` leaks the `Reference` objects themselves. ## Quick decision guide | Need | Use | |---|---| | Always keep | strong (normal) reference | | Cache, drop under memory pressure | `SoftReference` | | Auto-drop when key/value no longer used elsewhere | `WeakReference` / `WeakHashMap` | | Run cleanup after the object is collected | `PhantomReference` + `ReferenceQueue` (or `Cleaner`) |

  • What's the practical difference between SoftReference and WeakReference for a cache?
    Soft references survive until the JVM is short on memory (so the cache stays warm but is unpredictable and can grow until pressure hits), while weak references are dropped at the very next GC once nothing else holds the value — too aggressive for most caches. In practice a bounded LRU cache beats both.
  • Why prefer PhantomReference + Cleaner over finalize()?
    finalize() can resurrect the object, runs on an unspecified thread at an unspecified time, delays reclamation by an extra GC cycle, and is deprecated for removal. Phantom-reference/Cleaner cleanup can't resurrect, runs deterministically on a known worker thread after the object is unreachable, and won't keep the object alive.

saying these in an interview costs you the question

  • Saying WeakReference is cleared only under memory pressure (that's SoftReference)
  • Thinking PhantomReference.get() can return the object (it always returns null)
  • Treating SoftReference caches as precisely tunable — GC behavior is unspecified
  • Forgetting that one remaining strong reference defeats any weaker reference

context