Java WeakHashMap Internals: Preventing Memory Leaks with Weak References
Reference strengths, ReferenceQueue polling, expungeStaleEntries, and GC caching.
Part 20 in Series — Catch up on the previous article: High-Performance Java: How EnumSet and EnumMap Achieve Zero-Allocation Speed (Part 19) before diving into this post.
Suppose you are building a database ORM framework like Hibernate or an application monitoring agent.
Your monitoring agent attaches metadata context objects (request timestamps, transaction IDs) to active Thread or User instances:
Map<User, UserMetadata> metadataCache = new HashMap<>();
When a user logs out or leaves the system, your application drops all references to that User object. You expect the Java Garbage Collector to reclaim the memory.
Instead, your application crashes three hours later with java.lang.OutOfMemoryError: Java heap space.
Why? Because HashMap holds a strong reference to the User key object inside its internal bucket array node. Even though the rest of your application discarded the user, the map entry keeps the user object alive on the heap indefinitely.
To let the Garbage Collector automatically reclaim unused keys and evict map entries, Java provides WeakHashMap.
Why You Need This in Real Life
Standard collections hold strong references to their elements. As long as the collection exists, every element inside it is protected from Garbage Collection.
WeakHashMap breaks this retention lock:
- Automatic Cache Cleanup: When a key object is no longer referenced anywhere else in your application,
WeakHashMapallows the key to be garbage collected and automatically expunges the corresponding key-value entry. - Canonicalizing Mappings: Storing temporary metadata linked to external object lifecycles without causing memory leaks.
Reference Strengths in the HotSpot JVM
To understand WeakHashMap, you need to know Java’s reference hierarchy:
- Strong Reference (
User u = new User()): Standard Java references. GC will never reclaim an object reachable via strong references, even if heap memory runs out. - Soft Reference (
SoftReference<User>): GC reclaims soft-referenced objects only when heap memory is nearly exhausted. Useful for memory-sensitive caches. - Weak Reference (
WeakReference<User>): GC reclaims weak-referenced objects on the very next garbage collection cycle if no strong references exist.
STRONG REFERENCE (HashMap):
HashMap Table ---> Node ---> Key ("UserA" Strong Ref) ---> Heap Memory (GC CANNOT RECLAIM!)
WEAK REFERENCE (WeakHashMap):
WeakHashMap Table ---> Entry ---> WeakReference key --------> Heap Memory (GC RECLAIMS!)
|
v (When garbage collected)
ReferenceQueue
The ReferenceQueue Cleanup Mechanism
How does WeakHashMap know when a key object has been garbage collected so it can remove the dead entry from its bucket array?
Through a java.lang.ref.ReferenceQueue.
- Each bucket entry in
WeakHashMapextendsWeakReference<K>and registers itself with a sharedReferenceQueue. - When the Garbage Collector detects that a key object is no longer strongly reachable, it clears the weak reference and enqueues the
Entryobject into theReferenceQueue. - The next time
WeakHashMap.get(),put(), orsize()is called, the map polls theReferenceQueueand deletes matching bucket entries.
Simplified MyWeakHashMap<K, V> Implementation
Here is how WeakHashMap combines weak references with reference queues:
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
public class MyWeakHashMap<K, V> {
private static class Entry<K, V> extends WeakReference<K> {
V value;
final int hash;
Entry<K, V> next;
Entry(K key, V value, ReferenceQueue<K> queue, int hash, Entry<K, V> next) {
super(key, queue); // Registers weak key with reference queue!
this.value = value;
this.hash = hash;
this.next = next;
}
}
private Entry<K, V>[] table;
private int size;
private final ReferenceQueue<K> queue = new ReferenceQueue<>();
private static final int DEFAULT_CAPACITY = 16;
@SuppressWarnings("unchecked")
public MyWeakHashMap() {
this.table = (Entry<K, V>[]) new Entry[DEFAULT_CAPACITY];
}
public V put(K key, V value) {
if (key == null) throw new NullPointerException("Null keys not supported");
expungeStaleEntries(); // Clean up garbage-collected entries first!
int hash = hash(key);
int index = (table.length - 1) & hash;
Entry<K, V> e = table[index];
while (e != null) {
K k = e.get(); // Fetch key object from weak reference
if (e.hash == hash && key.equals(k)) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
e = e.next;
}
table[index] = new Entry<>(key, value, queue, hash, table[index]);
size++;
return null;
}
public V get(K key) {
if (key == null) return null;
expungeStaleEntries();
int hash = hash(key);
int index = (table.length - 1) & hash;
Entry<K, V> e = table[index];
while (e != null) {
if (e.hash == hash && key.equals(e.get())) {
return e.value;
}
e = e.next;
}
return null;
}
@SuppressWarnings("unchecked")
private void expungeStaleEntries() {
Object x;
// Polls dead entries enqueued by the Garbage Collector
while ((x = queue.poll()) != null) {
synchronized (queue) {
Entry<K, V> e = (Entry<K, V>) x;
int index = (table.length - 1) & e.hash;
Entry<K, V> prev = table[index];
Entry<K, V> p = prev;
while (p != null) {
Entry<K, V> next = p.next;
if (p == e) {
if (prev == e) table[index] = next;
else prev.next = next;
e.value = null; // Help GC clear value payload!
size--;
break;
}
prev = p;
p = next;
}
}
}
}
static final int hash(Object key) {
int h = key.hashCode();
return h ^ (h >>> 16);
}
}
Critical Gotcha: Value References Key
If a value object stored inside a WeakHashMap contains a strong reference back to its own key object, weak reference cleanup fails!
// BAD PRACTICE: Value holds strong reference to key!
class UserMetadata {
private final User owner; // Strong back-reference to key!
public UserMetadata(User owner) { this.owner = owner; }
}
map.put(userKey, new UserMetadata(userKey)); // Key can NEVER be garbage collected!
Because value holds a strong reference to userKey, userKey remains strongly reachable through the map value payload, neutralizing WeakHashMap eviction.
Quick Summary
WeakHashMapwraps keys inWeakReferenceobjects, allowing the Garbage Collector to reclaim key instances when unused elsewhere.- The JVM enqueues dead reference objects into a
ReferenceQueuewhen garbage collection occurs. WeakHashMappolls the queue duringget()andput()operations to expunge dead bucket entries automatically.
References & Further Reading
- Oracle Corporation. Java SE 21 API Specification:
java.util.ConcurrentModificationException. Oracle Docs. - OpenJDK Repository. OpenJDK 21 Source Code:
java.util.ArrayList.Itr. GitHub. - Bloch, J. (2018). Effective Java (3rd Edition) — Item 79: Avoid Excessive Synchronization. Addison-Wesley.
Part 21: Java IdentityHashMap Internals: Reference Equality & Open Addressing Probing
Continue to Part 21 →