What is a PhantomReference in Java, and why does its get() method always return null?
answer
- get() == null by design — no resurrection
- Weakest reference; phantom-reachable = only phantoms remain
- Always paired with a ReferenceQueue (enqueued after clear)
- Post-mortem notification, not access
- Replaces finalize(); foundation of Cleaner
basics
~20 sA PhantomReference is the weakest reference type. Its get() always returns null on purpose, so you can never resurrect the object. It only exists to tell you, via a ReferenceQueue, that the object has already been collected so you can run cleanup.
solid answer
~40 sPhantomReference is the weakest of Java's reference types (after soft and weak). Unlike them, get() always returns null by design, so you can never reach the referent through the phantom reference. Its sole purpose is post-mortem notification: you always create it with a ReferenceQueue, and once the garbage collector determines the referent is phantom-reachable (no strong/soft/weak path) and clears it, the GC enqueues the PhantomReference. Your code polls or blocks on that queue to learn the object is gone, then releases associated resources such as native memory or file handles. Because get() returns null, the object cannot be resurrected the way a finalizer could, making phantom-based cleanup safer and more deterministic than Object.finalize().
code
java · 14 linesReferenceQueue<Object> queue = new ReferenceQueue<>();
Object resource = new Object();
PhantomReference<Object> ref = new PhantomReference<>(resource, queue);
System.out.println(ref.get()); // ALWAYS null, even now
resource = null; // drop the only strong reference
System.gc();
// Some time later, on a cleanup thread:
Reference<?> dead = queue.remove(); // blocks until GC enqueues 'ref'
if (dead == ref) {
// referent is gone -> release native/off-heap resources here
}go deeper
Knows PhantomReference is a reference type and that get() returns null; may not know why or about the queue pairing.
Explains the resurrection-safety reason for null get(), the mandatory ReferenceQueue, and that it's used for cleanup notification.
Contrasts with weak/soft, describes phantom-reachability, the enqueue lifecycle, the don't-capture-the-referent rule, and the link to Cleaner.
Reasons about GC reachability ordering, why this design is safer/more deterministic than finalization, and when native-resource cleanup belongs in phantom/Cleaner vs. try-with-resources.
## Background: reachability and the garbage collector Java manages memory automatically. The **garbage collector (GC)** periodically finds objects that the program can no longer reach and reclaims their memory. An object is **strongly reachable** if you can follow ordinary references (local variables, fields, statics) from a live thread to it. Such objects are never collected. Java also offers three *non-strong* reference types in `java.lang.ref`, each a wrapper object holding a hidden pointer to a **referent** (the object it points to): - **SoftReference** — cleared only when the JVM is low on memory (good for caches). - **WeakReference** — cleared as soon as the referent is no longer strongly/softly reachable (good for canonicalizing maps, e.g. `WeakHashMap`). - **PhantomReference** — the weakest. A reference is **eligible to be cleared** when its referent is reachable *only* through references at or below its own strength. So an object is **phantom-reachable** when nothing strong, soft, or weak points to it any more — only phantom references remain. ## Why PhantomReference.get() always returns null `SoftReference.get()` and `WeakReference.get()` return the referent (or null once cleared), letting you *use* the object while it's still alive. `PhantomReference.get()` is **hard-coded to return null** — always, even right after you construct it. The reason is **resurrection safety**. The old `Object.finalize()` mechanism handed your code a live reference to a dying object inside the finalizer, which let buggy or malicious code *resurrect* it (store it somewhere reachable again), defeating collection and forcing the GC to track finalizable objects through a slow, two-cycle process. Phantom references deliberately refuse to expose the referent: you are told the object **has effectively already gone** (it is past the point of being reachable by application code), but you are given no way to touch it. So you cannot resurrect it. The phantom reference is purely a **notification token**, not an accessor. ## The mandatory ReferenceQueue pairing Because `get()` is useless, a PhantomReference is **always** constructed with a `ReferenceQueue`: ```java ReferenceQueue<Object> queue = new ReferenceQueue<>(); PhantomReference<MyResource> ref = new PhantomReference<>(resource, queue); ``` A **ReferenceQueue** is a thread-safe FIFO. When the GC decides the referent is phantom-reachable, it **clears** the reference and **enqueues** the PhantomReference onto its queue. (The same enqueue mechanism underpins soft and weak references too — they may optionally take a queue and are enqueued after being cleared; phantom references are the case where the queue is the *only* reason to create the reference.) Your code then drains the queue: ```java Reference<?> r = queue.remove(); // blocks until something is enqueued // the referent is gone; run cleanup tied to this ref ``` The usual trick is to subclass `PhantomReference` so the subclass *itself* holds the cleanup data (a native pointer, a file descriptor, a socket) — **never** a reference back to the referent, which would keep it alive forever and prevent the notification from ever firing. ## Where Cleaner fits Writing the queue-draining thread, the bookkeeping, and the don't-capture-the-referent discipline by hand is error-prone, so Java 9 added `java.lang.ref.Cleaner`, which packages all of this for you. You register `(object, cleanupRunnable)` and Cleaner internally creates a phantom reference + queue + background thread and runs your runnable after the object is collected. It is the modern, finalizer-free replacement for `Object.finalize()`. ## Key takeaways - PhantomReference is the weakest reference; `get()` is hard-null **by design** to prevent resurrection. - It is meaningless without a ReferenceQueue — the queue is how the GC notifies you *post-mortem*. - Use it (or, in practice, `Cleaner`) to release **native/off-heap resources**, not pure-Java memory (the GC already handles that).
- Why is it dangerous to keep a reference to the referent inside your PhantomReference subclass?It makes the referent strongly reachable through the phantom object, so it never becomes phantom-reachable, the reference is never enqueued, and your cleanup never runs — also a memory leak. Store only the resource handle (e.g. a native pointer), never the object itself.
- How is a PhantomReference different from a WeakReference for cache use?WeakReference.get() returns the live referent until cleared, so it works as a cache/canonical map. PhantomReference.get() is always null, so it cannot serve values — it is only for cleanup notification, not lookups.
saying these in an interview costs you the question
- Thinking get() returns the referent 'sometimes' or right after construction — it is always null.
- Believing a phantom reference lets you read or revive the object before it's collected.
- Storing a strong reference to the referent inside the PhantomReference subclass (keeps it alive forever).
- Confusing 'phantom-reachable' (only phantoms remain) with 'unreachable' generally.