📦 Section 4 · Question #17

What is WeakHashMap

If a key object inside a WeakHashMap is no longer reachable via any Strong References (StrongReference) throughout the entire application, the Java Garbage Collector (GC) automa...


🟢 Junior Level

1. What is WeakHashMap

WeakHashMap is a specialized implementation of the java.util.Map interface in which keys are stored using Weak References (java.lang.ref.WeakReference).

If a key object inside a WeakHashMap is no longer reachable via any Strong References (StrongReference) throughout the entire application, the Java Garbage Collector (GC) automatically reclaims that key during the next collection cycle. When this happens, WeakHashMap automatically evicts the corresponding key-value entry from the map:

Map<Object, String> weakMap = new WeakHashMap<>();

Object key = new Object(); // Strong reference created
weakMap.put(key, "Metadata for object");

System.out.println(weakMap.size()); // 1

key = null; // Strong reference destroyed; only the internal WeakReference remains
System.gc(); // Suggest JVM run garbage collection

// Upon the next map operation, the dead entry is expunged:
System.out.println(weakMap.size()); // 0

2. Primary Purpose: Associating Ephemeral Metadata

The core architectural purpose of WeakHashMap is binding auxiliary metadata or transient attributes to domain objects whose lifecycles are governed by external subsystems:

Real-World Example: Associating security contexts or state machines with network channels or UI widgets without altering their classes or risking memory leaks:

WeakHashMap<SocketChannel, SessionContext> channelSessions = new WeakHashMap<>();
// When the SocketChannel is closed and deregistered from the Selector,
// its SessionContext metadata is cleanly reclaimed automatically without manual cleanup!

🟡 Middle Level

1. Internal Architecture: ReferenceQueue and expungeStaleEntries()

A common misconception is that the Garbage Collector directly modifies the internal table[] array of WeakHashMap. In reality, the JVM and GC have no knowledge of collection bucket arrays.

The eviction lifecycle operates via cooperative, lazy cleanup:

  1. The Entry Extends WeakReference: The internal bucket node Entry<K,V> subclasses WeakReference<Object> and registers the key with an internal ReferenceQueue:
    private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        final int hash;
        Entry<K,V> next;
    
        Entry(Object key, V value, ReferenceQueue<Object> queue, int hash, Entry<K,V> next) {
            super(key, queue); // Registers key with the ReferenceQueue
            this.value = value;
            this.hash = hash;
            this.next = next;
        }
    }
    
  2. Garbage Collector Interaction: When the GC determines that the key is only weakly reachable, it clears the reference (referent = null) and appends the Entry object to the associated ReferenceQueue.

  3. Lazy Expunging (expungeStaleEntries()): WeakHashMap maintains zero background daemon threads. Eviction occurs on-demand whenever a public map method (get(), put(), remove(), size()) is invoked:
    private void expungeStaleEntries() {
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {
                Entry<K,V> e = (Entry<K,V>) x;
                int i = indexFor(e.hash, table.length);
                // Unlinks e from bucket table[i]
                // Explicitly nulls out e.value to allow garbage collection of the value!
                e.value = null; 
            }
        }
    }
    

2. The Fatal Trap: Cyclic Reference from Value to Key

If the value object stored in WeakHashMap directly or indirectly holds a strong reference back to its corresponding key, automatic garbage collection fails completely:

class OrderContext {
    private final Order order; // Strong reference back to key!
    public OrderContext(Order order) { this.order = order; }
}

WeakHashMap<Order, OrderContext> map = new WeakHashMap<>();
Order order = new Order(101);
map.put(order, new OrderContext(order)); // ARCHITECTURAL BUG

order = null; // External strong reference cleared
System.gc();

// SILENT MEMORY LEAK: Entry is NEVER collected!
System.out.println(map.size()); // 1

The Retaining Reference Chain: \(\text{GC Root} \longrightarrow \text{WeakHashMap.table} \longrightarrow \text{Entry} \overset{\text{Strong}}{\longrightarrow} \text{OrderContext (Value)} \overset{\text{Strong}}{\longrightarrow} \text{Order (Key)}\) Because the key is reachable via a continuous chain of strong references originating from the map’s own bucket array, the key remains Strongly Reachable. The GC never clears the weak reference, the entry is never enqueued, and the object leaks permanently.


🔴 Senior Level

1. Architectural Anti-Pattern: Why WeakHashMap Must NOT Be Used as a Cache

Using WeakHashMap as a general-purpose application or data cache is a severe architectural anti-pattern:

  1. Violent Eviction Frequency: Weak references are cleared on every single Minor GC (Young Generation sweep) as long as no external strong reference exists. In high-throughput services, Minor GCs occur every few hundred milliseconds. If you cache heavy database query results keyed by parameter objects, entries will vanish almost immediately after the calling method finishes, driving the cache hit rate to near zero and causing severe CPU thrashing.
  2. The Interned Key Trap: If keys are string literals ("USER_ROLE"), interned strings, or cached wrapper primitives (e.g., Integer.valueOf(42)), they are strongly held forever in JVM internal pools or static class loaders. They will never be garbage collected, turning WeakHashMap into an unbounded, leaking HashMap.
  3. Absence of Cache Eviction Policies: WeakHashMap lacks eviction algorithms (LRU, W-TinyLFU, TTL, max-size bounding, cost accounting).

[!TIP] For production caching, always use dedicated, battle-tested caching frameworks such as Caffeine (which uses the Window TinyLFU algorithm) or SoftReference-based caches that only evict elements under genuine JVM heap exhaustion.


2. The “Idle Map” Memory Leak

Because cleanup relies on lazy invocation within public methods, an idle map can trap significant memory:

  • If an application inserts 1,000,000 entries with heavy byte arrays as values into a WeakHashMap, and then stops calling methods on the map, the GC will collect the keys, but the table[] array continues holding strong references to all the value payloads.
  • The memory consumed by value objects will not be freed until a method on that specific WeakHashMap is called again or the entire map becomes unreachable.
  • For this reason, WeakHashMap.size() does not simply return a cached field; it first runs expungeStaleEntries() and counts remaining nodes.

4 Tricky Questions

1. Why can inserting a literal string key like "admin_session" in a WeakHashMap cause a permanent memory leak?

Answer: String literals in Java are automatically interned into the JVM String Constant Pool residing in the heap. Objects in the String Pool are held by strong references rooted in the system ClassLoader. Because an interned string is always strongly reachable, the GC will never clear its weak reference, and its entry in WeakHashMap will never be automatically evicted.


2. How and why does a strong reference from a Value back to its Key break WeakHashMap’s garbage collection?

Answer: WeakHashMap stores its entries in a standard table[] array, which strongly references each Entry<K,V> node. The Entry holds a strong reference to the value field. If that value holds a strong reference back to the key, a complete strong-reference path exists: table -> Entry -> value -> key. Because the key is reachable from a live GC Root through this strong chain, the key’s reachability is Strong, not Weak. Consequently, the GC never identifies the key as a candidate for collection, never nulls out the referent, and never pushes the entry to the ReferenceQueue.


3. Does WeakHashMap use a background daemon thread to monitor and clean up collected keys?

Answer: No. WeakHashMap employs no background threads. Eviction is entirely lazy and cooperative: the private method expungeStaleEntries() polls the map’s internal ReferenceQueue at the start of standard operations like get(), put(), remove(), and size(). If no methods are invoked on the map, dead entries and their strongly-held value objects remain resident in memory.


4. Why is size() in WeakHashMap not an $O(1)$ variable read like in HashMap?

Answer: In HashMap, size is an integer field updated during put() and remove(), returning in $O(1)$ time. In WeakHashMap, keys can be reclaimed by the garbage collector asynchronously without the map’s immediate knowledge. If size() simply returned the internal counter, it would report stale, collected keys. Therefore, WeakHashMap.size() first invokes expungeStaleEntries() to drain the ReferenceQueue and unlink dead entries before calculating and returning the actual count.


🎯 Interview Cheat Sheet

30-Second Elevator Pitch

“WeakHashMap is a hash table whose keys are wrapped in WeakReferences. When an object-key loses all external strong references, the GC sweeps the key and enqueues the internal Entry into a ReferenceQueue. Stale entries are then lazily purged during subsequent map operations via expungeStaleEntries(). It is specifically designed for associating auxiliary metadata with objects managed by external lifecycles. It must never be used as an application cache because entries are wiped on every Minor GC, and it creates permanent memory leaks if a value holds a strong reference back to its key.”

High-Impact Architectural Facts

  • Key-Only Weakness: Only keys are stored via WeakReference; values are strongly referenced by the entry until expungeStaleEntries() sets e.value = null.
  • Lazy Eviction: No background cleaner threads; dead entries are drained on-demand when calling get(), put(), remove(), or size().
  • Reference Loop Vulnerability: $\text{Key} \leftarrow \text{Value}$ strong loops disable weak reachability, permanently locking entries in memory.
  • Cache Anti-Pattern: Weak references are cleared aggressively on every Minor GC; use Caffeine for production caching.

Common Pitfalls & Red Flags

  • ❌ Using WeakHashMap as an in-memory cache: Data vanishes during routine Minor GC sweeps, driving hit rates to zero.
  • ❌ Using string literals or boxed primitives as keys: String literals in the String Pool and Integer.valueOf() (-128 to 127) are never collected.
  • ❌ Assuming WeakHashMap is thread-safe: It is completely unsynchronized; concurrent access without Collections.synchronizedMap() corrupts bucket node lists.
  • ❌ Assuming values are automatically collected: Values are held strongly; an idle map retains values until a method is called.