Reference Types (Weak, Soft, Phantom)
Java provides four reference strengths beyond the ordinary strong reference: strong (the default), soft, weak, and phantom. Each has a different policy for when the garbage collector is permitted to reclaim the referenced object. Soft references are collected when the JVM is low on memory — ideal for memory-sensitive caches. Weak references are collected at the next GC cycle — ideal for canonicalising maps and listeners. Phantom references are enqueued after the object is finalised but before its memory is reclaimed — ideal for post-collection cleanup actions. Understanding these reference types enables building caches that do not cause OutOfMemoryError, listeners that automatically deregister themselves, and deterministic cleanup of native resources. This entry covers all four strengths with their precise semantics, the ReferenceQueue mechanism, and the canonical use case for each type.
Reference Strength Hierarchy
// ── Reference strength hierarchy ─────────────────────────────────────
//
// Reachability levels (strongest to weakest):
//
// STRONG Object referenced by any variable/field without Reference wrapper
// → NEVER collected while any strong reference exists
//
// SOFT Object referenced only through SoftReference instances
// → Collected when JVM needs memory (before OOM)
// → Kept alive when memory is plentiful
// → Use for memory-sensitive caches
//
// WEAK Object referenced only through WeakReference instances
// → Collected at the next GC cycle (promptly)
// → Use for canonicalising maps, associations
//
// PHANTOM Object referenced only through PhantomReference instances
// → Already considered collected by the GC (get() returns null)
// → Reference enqueued AFTER object is freed
// → Use for post-collection cleanup notification
//
// UNREACHABLE Object reachable from no reference at all
// → Collected (or finalised then collected)
// ── Creating reference objects ────────────────────────────────────────
Object target = new Object();
// Strong (implicit):
Object strong = target; // strong reference — target cannot be collected
// Soft:
SoftReference<Object> soft = new SoftReference<>(target);
// Weak:
WeakReference<Object> weak = new WeakReference<>(target);
// Phantom (requires ReferenceQueue):
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantom = new PhantomReference<>(target, queue);
// phantom.get() ALWAYS returns null — no access to object
// ── Object becomes eligible when all strong references are removed ─────
target = null; // strong reference removed
strong = null; // last strong reference removed
// Now 'target' is only softly/weakly/phantomly reachable
// GC can now collect it according to reference strength policiesWeakReference — Prompt Collection
// ── WeakReference basic usage ────────────────────────────────────────
Object expensiveObject = new ExpensiveResource();
WeakReference<ExpensiveResource> weakRef =
new WeakReference<>((ExpensiveResource) expensiveObject);
// Use the object — ALWAYS null-check
ExpensiveResource resource = weakRef.get();
if (resource != null) {
resource.doWork(); // safe — still alive
// WARNING: resource could be GC'd after this check but before use
// in multithreaded code — always hold a local strong reference
} else {
// Object has been collected — handle accordingly
recreateIfNeeded();
}
// ── Thread-safe usage — hold local reference ──────────────────────────
public void safeAccess(WeakReference<ExpensiveResource> ref) {
ExpensiveResource local = ref.get(); // get() returns strong ref
if (local == null) return; // already collected
local.doWork(); // local holds it alive during this method
// local goes out of scope here — no longer held
}
// ── WeakHashMap — entries live as long as keys are reachable ──────────
WeakHashMap<Object, String> metadata = new WeakHashMap<>();
Object key1 = new Object();
Object key2 = new Object();
metadata.put(key1, "data for key1");
metadata.put(key2, "data for key2");
System.out.println(metadata.size()); // 2
key1 = null; // key1 object no longer strongly referenced
System.gc(); // encourage collection
// After GC: WeakHashMap entry for key1 is removed
System.out.println(metadata.size()); // 1 (key2 still alive)
// ── PITFALL: boxed integer keys are cached — never collected ───────────
WeakHashMap<Integer, String> badMap = new WeakHashMap<>();
badMap.put(42, "value"); // Integer.valueOf(42) is CACHED
System.gc();
System.out.println(badMap.size()); // Still 1 — Integer(42) never collected!
// Integer values -128 to 127 are permanently cached by the JVM
// They are always strongly reachable → WeakHashMap entries NEVER expire
// ── Weak listener pattern ─────────────────────────────────────────────
public class WeakListenerList<T> {
private final List<WeakReference<T>> listeners = new CopyOnWriteArrayList<>();
public void add(T listener) {
listeners.add(new WeakReference<>(listener));
}
public void notifyAll(Consumer<T> action) {
listeners.removeIf(ref -> {
T listener = ref.get();
if (listener == null) return true; // collected — remove
action.accept(listener);
return false;
});
}
}SoftReference — Memory-Sensitive Caches
// ── SoftReference basic usage ────────────────────────────────────────
byte[] largeData = computeExpensiveData();
SoftReference<byte[]> softRef = new SoftReference<>(largeData);
largeData = null; // release strong reference — only soft ref remains
// Use — check for collection
byte[] data = softRef.get();
if (data != null) {
process(data); // still in memory — cache hit
} else {
// Collected by GC under memory pressure — cache miss
data = computeExpensiveData(); // recompute
softRef = new SoftReference<>(data); // re-cache
data = null; // release strong reference again
}
// ── SoftReference cache implementation ───────────────────────────────
public class SoftReferenceCache<K, V> {
private final Map<K, SoftReference<V>> cache =
new ConcurrentHashMap<>();
private final Function<K, V> loader;
public SoftReferenceCache(Function<K, V> loader) {
this.loader = loader;
}
public V get(K key) {
SoftReference<V> ref = cache.get(key);
if (ref != null) {
V value = ref.get();
if (value != null) return value; // cache hit
cache.remove(key, ref); // ref cleared — remove stale entry
}
// Cache miss: load and cache
V value = loader.apply(key);
cache.put(key, new SoftReference<>(value));
return value;
}
public void invalidate(K key) {
cache.remove(key);
}
}
// Usage:
SoftReferenceCache<Long, ReportData> reportCache =
new SoftReferenceCache<>(id -> generateReport(id));
ReportData report = reportCache.get(orderId);
// ── SoftReference with ReferenceQueue — detect evictions ─────────────
ReferenceQueue<byte[]> evictionQueue = new ReferenceQueue<>();
Map<SoftReference<byte[]>, String> keyMap = new HashMap<>();
SoftReference<byte[]> ref =
new SoftReference<>(new byte[1024], evictionQueue);
keyMap.put(ref, "someKey"); // track which key this ref belongs to
// Poll the queue to detect evictions:
Reference<? extends byte[]> evicted;
while ((evicted = evictionQueue.poll()) != null) {
String key = keyMap.remove(evicted); // find the key
System.out.println("Evicted entry for key: " + key);
// Could reload or notify
}
// ── Better alternative: Caffeine with softValues ───────────────────────
Cache<Long, ReportData> cache = Caffeine.newBuilder()
.softValues() // values held by soft references
.maximumSize(10_000) // also bounded by count
.expireAfterWrite(1, TimeUnit.HOURS)
.recordStats() // monitor hit rate
.build(id -> generateReport(id)); // auto-loaderPhantomReference and ReferenceQueue
// ── PhantomReference for post-collection cleanup ─────────────────────
public class ResourceTracker {
private static final ReferenceQueue<Object> QUEUE =
new ReferenceQueue<>();
// Keeps PhantomReferences alive (otherwise they'd be GC'd too)
private static final Set<CleanupRef> LIVE_REFS =
Collections.synchronizedSet(new HashSet<>());
// Background thread processes cleanup notifications
static {
Thread cleanerThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
// Block until a PhantomReference is enqueued
CleanupRef ref = (CleanupRef) QUEUE.remove();
LIVE_REFS.remove(ref);
ref.cleanup(); // perform cleanup
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}, "phantom-cleaner");
cleanerThread.setDaemon(true);
cleanerThread.start();
}
static class CleanupRef extends PhantomReference<Object> {
private final String resourceId;
private final long nativeHandle;
CleanupRef(Object referent, String id, long handle) {
super(referent, QUEUE); // registered with the queue
this.resourceId = id;
this.nativeHandle = handle;
// CRITICAL: CleanupRef does NOT hold a reference to 'referent'
// (PhantomReference itself holds the phantom ref — not CleanupRef fields)
}
void cleanup() {
System.out.println("Cleaning up resource: " + resourceId);
releaseNative(nativeHandle);
}
}
public static void track(Object obj, String id, long handle) {
CleanupRef ref = new CleanupRef(obj, id, handle);
LIVE_REFS.add(ref); // prevent CleanupRef itself from being collected
}
}
// ── ReferenceQueue with WeakReference — stale entry removal ───────────
public class WeakValueCache<K, V> {
private final Map<K, WeakEntry<K, V>> map = new ConcurrentHashMap<>();
private final ReferenceQueue<V> queue = new ReferenceQueue<>();
static class WeakEntry<K, V> extends WeakReference<V> {
final K key;
WeakEntry(K key, V value, ReferenceQueue<V> queue) {
super(value, queue);
this.key = key;
}
}
public void put(K key, V value) {
expungeStaleEntries(); // clean up before adding
map.put(key, new WeakEntry<>(key, value, queue));
}
public V get(K key) {
expungeStaleEntries();
WeakEntry<K, V> entry = map.get(key);
return entry != null ? entry.get() : null; // null if collected
}
@SuppressWarnings("unchecked")
private void expungeStaleEntries() {
WeakEntry<K, V> stale;
while ((stale = (WeakEntry<K, V>) queue.poll()) != null) {
map.remove(stale.key, stale); // remove stale entry
}
}
}