🔑 Section 10 · Question #27

What is WeakHashMap

WeakHashMap is a specialized implementation of the Map interface that automatically discards a key-value mapping when the key object is no longer held by any strong references o...


🟢 Junior Level

WeakHashMap is a specialized implementation of the Map interface that automatically discards a key-value mapping when the key object is no longer held by any strong references outside the map and has been collected by the Garbage Collector (GC).

Key Characteristics:

  1. Weak References on Keys: Keys are wrapped in java.lang.ref.WeakReference. To the Garbage Collector, a weak reference does not prevent an object from being reclaimed.
  2. Strong References on Values: Values are held by strong references (StrongReference) until the entry is formally expunged from the map.
  3. Lazy Cleanup: Physical removal of dead entries from the internal table array does not happen instantaneously when GC runs; it happens lazily during subsequent method invocations on the map (get, put, size, etc.).
import java.util.Map;
import java.util.WeakHashMap;

public class WeakHashMapDemo {
    public static void main(String[] args) {
        Map<Object, String> map = new WeakHashMap<>();

        Object key = new Object(); // Strong reference to key
        map.put(key, "Metadata for Key");

        System.out.println("Before GC: " + map.size()); // 1

        key = null; // Discard external strong reference
        System.gc(); // Request GC (for educational demonstration)

        // Calling size() triggers lazy internal expunging
        System.out.println("After GC: " + map.size()); // 0 (entry automatically removed)
    }
}

Analogy: Imagine a hotel cloakroom where guest belongings are indexed by temporary electronic keycards. The moment a guest checks out of the hotel and surrenders their keycard (the strong reference disappears), their temporary items in the cloakroom are automatically routed to disposal without needing a separate manual request.


🟡 Middle Level

The WeakReference and ReferenceQueue Architecture

Under the hood, WeakHashMap defines its internal entry node by inheriting directly from WeakReference:

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value; // CAUTION: value is held via a STRONG reference!
    final int hash;
    Entry<K,V> next;

    Entry(Object key, V value, ReferenceQueue<Object> queue, int hash, Entry<K,V> next) {
        super(key, queue); // The key is registered with the ReferenceQueue
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }
}

Entry Eviction Lifecycle:

  1. GC Phase: The Garbage Collector detects that the key object has no remaining strong references.
  2. Reference Clearing & Enqueuing: The GC clears the weak reference (ref.get() == null) and places the Entry instance itself into the associated ReferenceQueue.
  3. Lazy Expunging via expungeStaleEntries(): Every subsequent call to map operations (get, put, size, remove, resize) invokes the queue drainage routine:
    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);
                // Unlink node e from bucket linked list table[i]
                // e.value = null; // Clear strong reference to value for GC!
            }
        }
    }
    
  4. Memory Reclamation: Setting e.value = null severs the strong reference holding the value, permitting the GC to reclaim the value object in the subsequent collection cycle.

Reference Reachability Levels in Java

Reference Type Class When Collected by GC Primary Use Case
Strong Default assignment (Object o = new Object()) Never while reachable from GC Roots Standard domain objects and state
Soft SoftReference<T> Only when JVM faces imminent OutOfMemoryError In-memory sensitive data caches
Weak WeakReference<T> During any GC cycle WeakHashMap, metadata registries
Phantom PhantomReference<T> After object finalization Post-mortem cleanup, off-heap buffers

🔴 Senior Level

The Fatal Value-to-Key Circular Strong Reference Leak

The most subtle and dangerous architectural hazard in WeakHashMap occurs when a value directly or indirectly holds a strong reference back to its key:

class UserMetadata {
    private final User owner; // Strong reference back to the key!
    public UserMetadata(User owner) { this.owner = owner; }
}

Map<User, UserMetadata> map = new WeakHashMap<>();
User user = new User("Alice");
map.put(user, new UserMetadata(user));

user = null; // Developer expects entry to be collected...
System.gc();

Why this creates a permanent memory leak:

  1. The map’s internal table[] array strongly references the Entry.
  2. The Entry.value field holds a strong reference to UserMetadata.
  3. UserMetadata.owner holds a strong reference back to the User object (the key).
  4. Result: The key remains strongly reachable from GC Roots via table -> Entry -> value -> key. The key never becomes weakly reachable, the GC never enqueues it into ReferenceQueue, and the entry permanently leaks in heap memory!

Absence of a Background Reaper Daemon

WeakHashMap contains no internal background reaper thread:

  • If you populate WeakHashMap with 1,000,000 entries holding large memory payloads.
  • Discard all external strong references to the keys.
  • The GC reclaims the keys and enqueues all 1,000,000 entries into ReferenceQueue.
  • But your application code never calls another method on this map instance:

All 1,000,000 value objects remain pinned in the heap indefinitely, because expungeStaleEntries() is only invoked from within read or write methods of WeakHashMap.

Why WeakHashMap Is NOT an Application Cache

Using WeakHashMap for caching HTTP responses or database queries is an architectural anti-pattern:

Feature WeakHashMap Production Cache (Caffeine / Guava)
Eviction Mechanism GC sweep timing only (unpredictable) W-TinyLFU / LRU / LFU algorithms
Time-to-Live (TTL / TTI) ❌ No built-in expiration ✅ Configurable expireAfterWrite / Access
Memory Bounding ❌ Unbounded (grows without limit) ✅ Strict limits (e.g., maximumSize(10_000))
Thread Safety ❌ Not thread-safe (requires lock wrapper) ✅ High-performance concurrent lock striping

Under heavy memory allocation pressure, the JVM Young Generation GC can run every few hundred milliseconds, aggressively flushing out valid cache items and devastating hit ratios. Production services should rely on Caffeine Cache.


4 Tricky Questions

1. What happens if you use string literals (e.g. "userId") as keys in WeakHashMap?

Answer: Entries associated with string literals will never be reclaimed by the Garbage Collector. String literals reside in the JVM String Pool, which maintains permanent strong roots until the hosting ClassLoader is unloaded. Because the string "userId" is always strongly reachable from the String Pool root, it never transitions to weakly reachable status. The entry stays in WeakHashMap permanently, causing it to act like a standard, unbounded HashMap. The same issue affects cached boxed primitives (such as Integer.valueOf(10) from the -128 to 127 integer cache).


2. Does WeakHashMap support Red-Black Trees (TreeNode) under high collision loads?

Answer: No. Unlike HashMap, which was upgraded in Java 8 to convert bucket linked lists into balanced Red-Black Trees (TreeNode) when chain length exceeds 8, WeakHashMap retains the legacy Java 1.2 architecture. Buckets in WeakHashMap are strictly singly-linked lists. Under severe hash collisions, lookups in WeakHashMap degrade to unconditional $O(N)$ linear time.


3. Can WeakHashMap.size() change between two consecutive lines of code without any mutating method calls?

Answer: Yes. Consider the following execution:

int size1 = map.size(); // 1. Invokes expungeStaleEntries(), returns 10
// GC runs right at this microsecond and collects 5 unreachable keys!
int size2 = map.size(); // 2. Invokes expungeStaleEntries(), unlinks 5 entries, returns 5

Even though no application thread called put() or remove(), the map’s observed size drops from 10 to 5 because of background Garbage Collection and the subsequent lazy queue draining executed by the second size() call.


4. How can you design a bidirectional key-value relationship in WeakHashMap without creating a memory leak?

Answer: If the value must retain a reference back to its key, wrap the back-reference inside the value with a WeakReference:

class SafeMetadata {
    private final WeakReference<User> userRef; // Weak back-reference prevents cycle!

    public SafeMetadata(User user) {
        this.userRef = new WeakReference<>(user);
    }

    public User getUser() {
        return userRef.get();
    }
}

The resulting reference chain table -> Entry -> value -> WeakReference -> key contains no strong link to key. When external strong references to User are removed, the GC smoothly reclaims the key and enqueues the Entry for deletion.


🎯 Interview Cheat Sheet

  • Core Mechanism: Keys are wrapped in WeakReference. When no strong references to a key exist outside the map, the GC reclaims the key, and the entry is unlinked.
  • Values Are Strongly Held: The Entry.value field is a strong reference.
  • The Value-to-Key Trap: If a value points back to its key via a strong reference, a circular strong chain table -> Entry -> value -> key forms, permanently leaking memory.
  • Lazy Cleanup: No background thread cleans the map; dead entries are drained only when map methods call expungeStaleEntries().
  • String Pool Gotcha: Using string literals or cached primitives as keys disables weak collection because the JVM holds permanent strong roots.
  • Not for Caching: Lacks TTL, LRU/LFU, bounded sizing, and concurrency; use Caffeine for production caching.