Definition
- WeakReference keys – every key is wrapped in a
WeakReference; once no strong reference to the key exists outside the map, the GC may collect it - Automatic entry removal – after the key is collected, the map removes the entry during its next internal operation via a
ReferenceQueue - Same API as HashMap – implements
Mapwithput,get,remove,size, and iteration - Not thread-safe – requires external synchronisation or
Collections.synchronizedMapfor concurrent access - Null keys and values – both are permitted, unlike
ConcurrentHashMap
At National Bank of India, every loan officer (key) has a folder of loan metadata (value) in a shared cabinet (WeakHashMap). When an officer retires and nobody in the bank holds their employee badge any more (no strong reference), the cleaning crew (garbage collector) shreds the badge and the cabinet automatically removes that officer's folder. If the officer is still referenced by any active team, the folder stays. This keeps the cabinet from growing forever with stale data.
Understanding the Concepts
What is WeakHashMap?
WeakHashMap is a Map implementation in java.util where each key is stored inside a WeakReference. When no strong reference to the key exists outside the map, the garbage collector is free to collect it. On the next map operation, the entry is silently removed.
This differs from HashMap, which holds strong references to keys. In a HashMap, entries persist until explicitly removed, even when the key object is no longer used anywhere else in the program.
package com.bank.demo;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
public class WeakHashMapBasicDemo {
public static void main(String[] args) {
Map<Object, String> weak = new WeakHashMap<>();
Object accountKey = new Object();
weak.put(accountKey, "Rajesh's loan metadata");
System.out.println("Before nulling key: size = " + weak.size());
accountKey = null; // remove the only strong reference
System.gc(); // hint to the GC
// Allow GC time to run
try { Thread.sleep(200); } catch (InterruptedException ignored) {}
System.out.println("After GC: size = " + weak.size());
}
}
Before nulling key: size = 1
After GC: size = 0
Why does WeakHashMap exist?
WeakHashMap solves a specific problem: memory leaks caused by long-lived maps that hold references to objects the rest of the application has abandoned. Without weak keys, a cache or metadata map keeps every key alive forever, even if no other part of the program uses it.
In a banking application, consider a cache that maps ClassLoader instances to configuration metadata. When a microservice module is hot-reloaded, the old ClassLoader should be garbage-collected. A HashMap would prevent that because it holds a strong reference to the ClassLoader key. A WeakHashMap allows the old ClassLoader to be collected, and the stale entry disappears automatically.
package com.bank.demo;
import java.util.Map;
import java.util.WeakHashMap;
public class ClassLoaderCacheDemo {
// Simulates a ClassLoader-scoped cache
private static final Map<ClassLoader, String> configCache = new WeakHashMap<>();
public static void main(String[] args) {
ClassLoader loader = new ClassLoader() {};
configCache.put(loader, "db.url=jdbc:oracle:thin:@nbi-prod:1521:BANKING");
System.out.println("Config cached: " + configCache.size());
loader = null;
System.gc();
try { Thread.sleep(200); } catch (InterruptedException ignored) {}
System.out.println("After classloader GC: " + configCache.size());
}
}
Config cached: 1
After classloader GC: 0
How does WeakHashMap work internally?
WeakHashMap maintains an array of buckets, similar to HashMap. Each entry wraps the key in a WeakReference that is registered with a ReferenceQueue. When the GC collects a weakly-reachable key, the JVM enqueues the reference into that queue.
Before every get, put, size, or iteration, WeakHashMap calls an internal expungeStaleEntries() method. This method drains the ReferenceQueue and removes every entry whose key has been collected.
- The key is stored as
new Entry<>(key, value, queue, hash, next)where Entry extendsWeakReference. expungeStaleEntries()polls the queue and removes matching entries from the bucket chains.- Cleanup is lazy: if no map operations are called, stale entries remain until the next call.
package com.bank.demo;
import java.lang.ref.WeakReference;
import java.lang.ref.ReferenceQueue;
public class ReferenceQueueDemo {
public static void main(String[] args) throws InterruptedException {
ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object key = new Object();
WeakReference<Object> weakRef = new WeakReference<>(key, queue);
System.out.println("Before GC: ref.get() = " + weakRef.get());
key = null;
System.gc();
Thread.sleep(200);
System.out.println("After GC: ref.get() = " + weakRef.get());
System.out.println("Queue poll: " + (queue.poll() != null ? "enqueued" : "empty"));
}
}
Before GC: ref.get() = java.lang.Object@<hashcode>
After GC: ref.get() = null
Queue poll: enqueued
How is WeakHashMap different from HashMap?
| Feature | HashMap | WeakHashMap |
|---|---|---|
| Key reference type | Strong | Weak (WeakReference) |
| Entry removal | Manual only (remove, clear) | Automatic when key is GC-collected |
| Null keys | One null key allowed | One null key allowed |
| Thread safety | Not thread-safe | Not thread-safe |
| Use case | General-purpose map | Caches, metadata, listener maps |
| Memory leaks | Possible if keys not removed | Self-cleaning |
| Performance overhead | None | Slight (ReferenceQueue polling) |
package com.bank.demo;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
public class HashMapVsWeakHashMapDemo {
public static void main(String[] args) throws InterruptedException {
Map<Object, String> strong = new HashMap<>();
Map<Object, String> weak = new WeakHashMap<>();
Object key1 = new Object();
Object key2 = new Object();
strong.put(key1, "Strong entry");
weak.put(key2, "Weak entry");
key1 = null;
key2 = null;
System.gc();
Thread.sleep(200);
System.out.println("HashMap size after GC: " + strong.size());
System.out.println("WeakHashMap size after GC: " + weak.size());
}
}
HashMap size after GC: 1
WeakHashMap size after GC: 0
What are the rules and constraints?
WeakHashMap has several important constraints that developers must understand.
- String literal keys are never collected. String literals live in the string pool with a strong reference from the JVM. A WeakHashMap entry with a string literal key behaves like a permanent entry.
- Value-to-key back-references cause leaks. If the value object holds a strong reference to its own key, the key is always strongly reachable through the chain: map entry value holds strong ref to key. The GC never collects the key, and the entry never disappears.
- Cleanup is lazy. Stale entries are only removed when the map is accessed. If no operations are performed, stale entries linger.
- Not thread-safe. Concurrent access requires external synchronisation.
package com.bank.demo;
import java.util.Map;
import java.util.WeakHashMap;
public class WeakHashMapPitfallsDemo {
public static void main(String[] args) throws InterruptedException {
Map<String, String> weak = new WeakHashMap<>();
// Pitfall 1: String literals are NEVER collected
weak.put("SAV2001", "Rajesh"); // string literal in pool
System.gc();
Thread.sleep(200);
System.out.println("Literal key after GC: " + weak.get("SAV2001"));
// Correct: use new String() for weak behavior
String heapKey = new String("CUR3001");
weak.put(heapKey, "Priya");
heapKey = null;
System.gc();
Thread.sleep(200);
System.out.println("Heap key after GC: " + weak.get(new String("CUR3001")));
}
}
Literal key after GC: Rajesh
Heap key after GC: null
What are common mistakes?
Mistake 1: Using String literals as keys and expecting collection
// WRONG: String literal is interned, never GC-collected
Map<String, String> cache = new WeakHashMap<>();
cache.put("ACCOUNT_METADATA", "some data");
// This entry will NEVER be removed by GC.
// CORRECT: Use new String() if you need the key to be collectible
Map<String, String> cache = new WeakHashMap<>();
String key = new String("ACCOUNT_METADATA");
cache.put(key, "some data");
// When 'key' goes out of scope, the entry can be collected.
Mistake 2: Value holding a reference back to the key
// WRONG: value references the key, preventing collection
Map<Object, Object[]> cache = new WeakHashMap<>();
Object myKey = new Object();
cache.put(myKey, new Object[]{myKey, "data"}); // value holds myKey!
myKey = null;
System.gc();
// Entry is NOT collected because value[0] is a strong ref to the key.
// CORRECT: wrap the back-reference in a WeakReference
import java.lang.ref.WeakReference;
Map<Object, WeakReference<Object>> cache = new WeakHashMap<>();
Object myKey = new Object();
cache.put(myKey, new WeakReference<>(myKey));
myKey = null;
System.gc();
// Now the key can be collected.
What are the edge cases?
Edge cases in WeakHashMap often surprise developers who expect it to behave exactly like HashMap.
package com.bank.demo;
import java.util.Map;
import java.util.WeakHashMap;
public class WeakHashMapEdgeCasesDemo {
public static void main(String[] args) throws InterruptedException {
Map<Object, String> weak = new WeakHashMap<>();
// Edge case 1: size() can change between calls without any explicit removal
Object k1 = new Object();
Object k2 = new Object();
weak.put(k1, "A");
weak.put(k2, "B");
k1 = null;
System.gc();
Thread.sleep(200);
System.out.println("Size might shrink: " + weak.size());
// Edge case 2: iteration can see fewer entries than expected
weak.clear();
for (int i = 0; i < 5; i++) {
weak.put(new Object(), "val" + i); // keys immediately eligible for GC
}
System.gc();
Thread.sleep(200);
System.out.println("Entries after GC of keyless objects: " + weak.size());
// Edge case 3: null key is always strongly held
weak.put(null, "null-key-value");
System.gc();
Thread.sleep(200);
System.out.println("Null key survives GC: " + weak.get(null));
}
}
Size might shrink: 1
Entries after GC of keyless objects: 0
Null key survives GC: null-key-value
When should you use WeakHashMap?
Use WeakHashMap when you need a cache or registry whose entries should automatically disappear when the key is no longer referenced by the application. Typical use cases include ClassLoader metadata caches, listener or observer registries, per-object memoisation, and expensive computation caches tied to transient keys.
When should you NOT use WeakHashMap?
Do not use WeakHashMap as a general-purpose map. If you need entries to persist reliably, use HashMap or ConcurrentHashMap. Do not use it when keys are String literals or other interned objects because those keys are never collected. Do not use it in concurrent code without synchronisation.
What about thread safety and concurrency?
WeakHashMap is not synchronised. In a multi-threaded banking application where multiple threads read account metadata from a shared cache, you must wrap it.
package com.bank.demo;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
public class SynchronizedWeakHashMapDemo {
private static final Map<Object, String> cache =
Collections.synchronizedMap(new WeakHashMap<>());
public static void main(String[] args) {
Object key = new Object();
cache.put(key, "Rajesh's account metadata");
System.out.println("Synchronized access: " + cache.get(key));
}
}
Synchronized access: Rajesh's account metadata
For high-concurrency scenarios, consider Guava's CacheBuilder.weakKeys() or Caffeine cache, which provide weak-key semantics with built-in concurrency, eviction policies, and statistics.
What should a senior engineer know?
- GC is non-deterministic.
System.gc()is only a hint. In production, you cannot predict when entries will be removed. Design for eventual cleanup, not immediate cleanup. - Soft references vs weak references.
SoftReferencekeys (not available in the JDK as a map, but possible with Guava) are collected only under memory pressure, making them better for caches. Weak references are collected at the next GC cycle. - WeakHashMap is O(1) amortised for
getandput, butexpungeStaleEntries()may iterate stale entries, adding occasional overhead. - Heap dumps. When diagnosing memory issues with VisualVM or Eclipse MAT, look at the
ReferenceQueuesize to check whether cleanup is keeping up with collection.
What are common interview follow-up questions?
- "What type of reference does WeakHashMap use for keys?" : WeakReference, registered with a ReferenceQueue.
- "Can String literal keys be collected?" : No. String literals are interned and always have a strong reference from the string pool.
- "What happens if the value references the key?" : The key can never be collected, causing a memory leak.
- "Is WeakHashMap thread-safe?" : No. Use Collections.synchronizedMap or an alternative cache library.
- "When is the stale entry actually removed?" : During the next map operation, when expungeStaleEntries drains the ReferenceQueue.
Visual Overview
WeakHashMap Internal Flow ======================== put(key, value): ┌────────────────────────────────────────────┐ │ 1. Wrap key in WeakReference(key, refQueue) │ │ 2. Compute hash: hash(key.hashCode()) │ │ 3. Find bucket: hash & (table.length - 1) │ │ 4. Insert Entry into bucket chain │ └────────────────────────────────────────────┘ GC collects key (no strong refs remain): ┌─────────────────────────────────────────┐ │ JVM enqueues WeakReference into refQueue │ └─────────────────────────────────────────┘ Next map operation (get, put, size): ┌────────────────────────────────────────────────┐ │ expungeStaleEntries(): │ │ while (queue.poll() != null) │ │ find entry in bucket chain │ │ remove entry from chain │ │ size-- │ └────────────────────────────────────────────────┘ Strong ref alive Strong ref removed ┌──────────┐ ┌──────────┐ │ variable ──strong──▶ key│ variable = null │ │ │ │ │ WHM entry──weak───▶ key│ WHM entry──weak───▶ (collected) │ │ │ │ │ Entry stays in map │ Entry auto-removed └──────────┘ └──────────┘
Key Takeaway
WeakHashMap is a specialised Map where keys are held via WeakReference. When no strong reference to a key exists outside the map, the garbage collector can reclaim it, and the map silently removes the entry during its next operation. This self-cleaning behaviour makes it ideal for caches, metadata maps, and listener registries that should not outlive the objects they describe.
The most common pitfall is using String literals as keys. Because the JVM's string pool maintains a strong reference to every literal, those entries are effectively permanent. Another trap is a value that holds a strong reference back to its key, which forms a reference cycle that prevents collection. Both mistakes silently convert a self-cleaning map into a memory-leaking map.
In the National Bank of India's microservice platform, Rajesh used WeakHashMap to cache ClassLoader-to-configuration mappings. When a module was hot-reloaded and the old ClassLoader became unreachable, its configuration entry disappeared automatically, preventing the kind of permgen or metaspace leaks that plague long-running banking servers.
Remember one sentence: WeakHashMap keys are weak references, so entries vanish when no strong reference to the key exists, but string literals and value-to-key back-references silently prevent that cleanup.
---