The WeakHashMap Class
Objective
Understand WeakHashMap, the AbstractMap implementation that stores each key inside a WeakReference instead of holding it directly: once nothing outside the map keeps a strong reference to a key, the JVM is free to reclaim it, and the map removes that entry on its own — trading a normal Map's guarantee that a key survives as long as it's in the map for automatic cleanup once nothing else needs the key.
Use Cases
- Per-object metadata or computed properties (framework internals attaching extra state to an application object) that should disappear the moment the object itself becomes unreachable, without an explicit "unregister" call.
- Listener/observer registries that shouldn't be the reason a listener outlives whatever registered it.
Class-keyed caches (frameworks associating data with aClass<?>) so a cache entry doesn't pin its classloader in memory after everything else using that class is gone.- Not a general-purpose size/TTL-bounded cache — a
WeakHashMaponly reacts to key reachability, never to memory pressure; that job belongs toSoftReference-based caching instead.
Deep Dive
WeakHashMap extends AbstractMap, implements Map
javaclass WeakHashMap<K, V>Four constructors, mirroring HashMap's:
javaWeakHashMap<Object, String> a = new WeakHashMap<>(); // default capacity 16, load factor 0.75
WeakHashMap<Object, String> b = new WeakHashMap<>(existingMap); // initialized from another Map
WeakHashMap<Object, String> c = new WeakHashMap<>(64); // initial capacity 64
WeakHashMap<Object, String> d = new WeakHashMap<>(64, 0.5f); // capacity 64, load factor 0.5WeakHashMap sits next to HashMap under AbstractMap, not underneath it — unlike LinkedHashMap, it is not a HashMap subclass, even though both use a hash table internally:
Each key lives inside a WeakReference, not as a plain field
Internally, every entry holds its key through a WeakReference<K> registered against the map's own ReferenceQueue. As long as something outside the map keeps a strong reference to the key, the entry behaves exactly like a HashMap entry. The moment that last strong reference goes away, the key becomes eligible for collection independently of the map holding it:
javaMap<Object, String> registry = new WeakHashMap<>();
Object key = new Object();
registry.put(key, "metadata");
System.out.println(registry.size()); // 1
key = null; // drop the only external strong reference to the key
System.gc(); // request a collection — not a hard guarantee, but reliable enough to observe here
System.out.println(registry.size()); // very likely 0 nowCleanup piggybacks on later map operations, not on the GC itself
The GC clears the WeakReference and enqueues it on the map's ReferenceQueue, but the entry isn't removed at that instant — nothing is watching the queue in the background. Removal happens lazily, the next time almost any map operation runs, because most of them call an internal expungeStaleEntries() first:
javaMap<Object, String> registry = new WeakHashMap<>();
Object key = new Object();
registry.put(key, "metadata");
key = null;
System.gc();
// size() itself triggers the expunge pass before it counts, so this already reflects the collected key:
System.out.println(registry.size()); // very likely 0
// isEmpty(), get(), put(), containsKey(), keySet()/entrySet() iteration all trigger the same pass —
// but nothing purges stale entries on its own between calls, so a WeakHashMap that is never touched
// again can sit holding cleared-but-not-yet-removed entries indefinitely.Trade-offs
No guarantee of exactly when an entry disappears — cleanup depends on GC timing plus a later map operation triggering the expunge pass, so
size()/iteration can briefly lag behind actual key reachability; never rely on the count for correctness-critical logic, only as a memory-management convenience.A value that strongly references its own key (directly or transitively) defeats the whole mechanism — the map's value slot is an ordinary strong reference, so if the value keeps the key reachable, the entry can never become eligible for collection:
javaMap<Object, Object> m = new WeakHashMap<>(); Object key = new Object(); m.put(key, key); // value == key -> the map itself now strongly holds the key via the value field key = null; System.gc(); System.out.println(m.size()); // still 1 -- the entry leaks for as long as the map existsCanonical/interned keys never get collected, silently defeating the point — a
Stringliteral, a cached boxedIntegerinInteger.valueOf's range, or anenumconstant is already held strongly elsewhere (the string pool, the JVM's boxing cache, the enum's own static fields), so wrapping it in aWeakHashMapkey buys nothing:javaMap<String, String> cache = new WeakHashMap<>(); cache.put("hello", "cached value"); // "hello" is interned -> permanently strongly reachable System.gc(); System.out.println(cache.size()); // 1, forever -- the string pool keeps the key aliveNot synchronized, and allows one
nullkey — same caveat asHashMap; concurrent access needs external synchronization or a different structure.