Java Reference Types: Weak, Soft, and Phantom References
Java has four reference strengths that control how aggressively the garbage collector reclaims objects. Understanding them lets you build memory-sensitive caches, prevent memory leaks in maps, and perform post-GC cleanup without relying on the deprecated finalize() method.
Contents
The Four Reference Strengths
- Strong reference: the default (
). The GC never collects a strongly reachable object.Object o = new Object() - Soft reference (
): GC collects when memory is low (before throwingSoftReference<T> ). Good for memory-sensitive caches.OutOfMemoryError - Weak reference (
): GC collects at the next collection cycle when only weakly reachable. Good for canonicalizing mappings (e.g., interning).WeakReference<T> - Phantom reference (
): GC enqueues it after the object is finalized but before memory is reclaimed. Used for post-GC resource cleanup.PhantomReference<T>
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: Memory-Sensitive Caches
SoftReferenceOutOfMemoryError
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);
}
}
ReferenceQueuenew SoftReference<>(obj, queue)WeakReference: Canonicalizing Mappings
WeakReference
// 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: Post-GC Cleanup
PhantomReference.get()nullReferenceQueue
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;
}
}
});
PhantomReferenceJava CleanerWeakHashMap
WeakHashMapWeakReference
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.
WeakHashMap