Contents
- The Four Reference Strengths
- SoftReference — Memory-Sensitive Caches
- WeakReference — Canonicalizing Mappings
- PhantomReference — Post-GC Cleanup
- WeakHashMap
- 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.
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 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.
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 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.
// 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.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.
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 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.
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.