WeakHashMap in Java — Stale Keys and OutOfMemoryError
3 a.m.
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
- WeakHashMap holds keys via WeakReference, enabling automatic eviction when no strong reference exists.
- Core components: WeakReference, ReferenceQueue, and expungeStaleEntries() cleanup.
- Performance: get/put O(1) amortized, but cleanup adds overhead after GC cycles.
- Production insight: Stale entries are removed lazily — memory isn't freed until a map operation triggers cleanup.
- Biggest mistake: Using WeakHashMap as a cache for frequently accessed keys — it evicts based on key liveness, not memory pressure.
Imagine you're a librarian who writes down which books people are currently reading. The moment someone returns a book and nobody else holds a copy, you automatically erase that book from your notes — you don't need to track it anymore. WeakHashMap works exactly like that: it holds references to keys, but the moment nothing else in your program is using a key, Java's garbage collector erases it from the map automatically. You never have to manually clean up. It's a map that knows when to forget.
Memory leaks are one of the nastiest production bugs in Java. You add entries to a cache, the keys go stale, but the map keeps holding onto them — the heap bloats, GC pressure climbs, and eventually you're staring at an OutOfMemoryError at 3 a.m. Most developers reach for HashMap by default without realising there's a data structure built specifically to prevent this class of problem. WeakHashMap is that structure, and understanding it deeply separates developers who react to memory leaks from those who design systems that never have them in the first place.
WeakHashMap solves the problem of strong-reference retention. In a regular HashMap, a key stored in the map is strongly reachable, which means the GC will never collect it as long as the map itself is alive. This is perfect for most cases, but catastrophic for caches where the map acts as the only reason a key object survives. WeakHashMap flips this contract: keys are held with weak references, so the GC is free to collect a key the moment no other strong reference to it exists anywhere else in your program. When a key is collected, its entry is silently removed from the map.
By the end of this article you'll understand exactly how WeakHashMap is implemented under the hood using WeakReference and ReferenceQueue, when it's the right tool and when it'll betray you, how it behaves under GC pressure with concrete runnable examples, and the three production gotchas that trip up even experienced engineers. You'll also walk away ready to answer the WeakHashMap questions that show up in senior Java interviews.
Why WeakHashMap Exists — and Where It Breaks
WeakHashMap is a Map implementation where keys are held by weak references. When a key becomes only weakly reachable — meaning no strong references exist outside the map — the garbage collector can reclaim it. The entry is then automatically removed from the map on the next operation. This is not a caching layer; it's a lifecycle management tool.
The core mechanic: WeakHashMap uses ReferenceQueue to track reclaimed keys. After a GC cycle, stale entries are expunged during subsequent map operations (get, put, size, etc.). This means map size can shrink without explicit removal. But note: values are held by strong references. If a value strongly references its own key, the key never becomes weakly reachable — a classic self-referential leak.
Use WeakHashMap when you need to associate metadata with objects whose lifetimes you don't control — for example, per-thread diagnostics or component-scoped caches in a DI container. It prevents the map from preventing GC of its keys. But it is not a general-purpose cache: entries vanish silently, and the map is not thread-safe.
How WeakHashMap Works Internally
WeakHashMap wraps keys in java.lang.ref.WeakReference. When the key object loses all strong references, the GC clears the weak reference and enqueues it into an internal ReferenceQueue. On every subsequent map operation (get, put, size, etc.), the map polls the queue and removes the corresponding entries. This is the expungeStaleEntries() method — it runs lazily, not in a background thread.
Let's walk through a runnable example that demonstrates eviction:
System.gc() is only a hint. In production, you don't control GC timing.size() or containsKey() periodically if latency-sensitive operations follow.When to Use (and Not Use) WeakHashMap
WeakHashMap is the right choice when the map's keys are objects with well-defined lifespans tied to application logic — for example, a per-request cache where keys are request-scoped objects. It's a poor fit for caches that need time-based or memory-based eviction, or when keys are value objects like Strings that are often interned or reused.
Here's a practical decision tree:
Comparing WeakHashMap with Other Map Types
Java offers several map implementations with different reference semantics. The key difference is how each handles key reachability: - HashMap: strong references to keys and values - WeakHashMap: weak references to keys (strong to values) - IdentityHashMap: uses reference equality (==) instead of equals() - EnumMap: for enum keys, high performance
For caching, WeakHashMap is often compared with SoftReference-based maps (not in JDK standard library). SoftReference allows collection only when memory is low, making it better for memory-sensitive caches.
Strong, Soft, and Weak References — The GC Trinity You Ignore at Your Peril
WeakHashMap doesn't make sense until you understand Java's reference hierarchy. Most devs only know strong references — the default. That's the problem. A strong reference is an iron chain: as long as it exists, GC can't touch the object. Integer prime = 1; — that's a strong reference. The object stays in memory until you explicitly null it or it goes out of scope.
Soft references are a pressure valve. JVM only collects them when it's about to run out of heap. They're useful for memory-sensitive caches that can survive GC cycles as long as there's breathing room. SoftReference<ImageCache> cacheRef = new SoftReference<>(new ImageCache());
Weak references are the real star here. GC will collect a weakly-reachable object on the very next cycle — no mercy, no hesitation. That's exactly what WeakHashMap uses for its keys. When your application code loses all strong references to the key object, GC evicts it immediately, and the map entry disappears. No manual cleanup. No memory leak. This is the mechanism that makes WeakHashMap's automatic eviction work, and understanding it is the difference between using this tool and getting burned by it.
System.gc() runs immediately. This demo shows the behavior conceptually — in production, GC timing is non-deterministic. WeakHashMap relies on the GC eventually clearing, not instantly.WeakHashMap as an Efficient Memory Cache — What Nobody Tells You About Eviction Timing
The most common use case for WeakHashMap is a cache that should not prevent garbage collection. Think metadata: class loaders, thread-local configs, or session-scoped data that should disappear when the parent object goes away. The classic example is a class loader: you cache per-class metadata, and when the class loader is garbage collected, the cache entries vanish automatically.
But here's the catch — WeakHashMap is not a time-based or size-based cache. Eviction happens when GC runs and the key is weakly reachable. You have zero control over when that happens. It could be 5 milliseconds or 5 minutes. This makes WeakHashMap unsuitable for caches where you need predictable eviction (like LRU). Use Caffeine or Guava for that.
Another trap: if you use String literals as keys, they are interned and have a strong reference from the string pool. Your entries will never be evicted. The same applies to Integer objects within the cached range (-128 to 127). Always use custom objects or new String("value") if you want weak key semantics. Test your eviction assumptions under a real GC profile before deploying.
Constructors: The Five Ways You'll Instantiate WeakHashMap
You don't just declare a WeakHashMap and move on. The constructor you pick dictates memory behavior, resizing frequency, and cache eviction semantics. Get it wrong, and you'll either waste heap or starve your map.
The default constructor gives you initial capacity 16 and load factor 0.75. Fine for prototypes. In production, every resize triggers a full GC run on the reference queue — so if you know you'll hold 10,000 entries, pass new WeakHashMap<>(10000) to avoid cascading rebuilds. The load factor constructor is rarely your friend; cranking it above 0.75 makes the map denser, which increases collision chains and slows key lookup. The map copy constructor is a trap: it clones a snapshot, but entries in the source map can be GC'd immediately after — your new map won't re-check references until the next expungeStaleEntries() call. Never use it for long-lived copies.
Why WeakHashMap Is Not a Cache (and What to Use Instead)
Every new developer assumes WeakHashMap is a free memory cache. It's not. It's a map with keys that die when the JVM says so — not when you want them to. GC runs are unpredictable, so eviction timing is nondeterministic. If you need a cache with TTLs, LRU eviction, or max-size bounds, WeakHashMap will betray you.
WeakHashMap shines for ephemeral metadata: per-thread request context, temporary listeners, or object-scoped properties. The keys are short-lived objects that you expect to die naturally. For caches, use Guava's Cache or Caffeine. They offer explicit eviction policies, expiration after write, and size-based limits. WeakHashMap's "eviction" is when the GC thread decides to run—not when your application needs memory. The only thing worse than a memory leak is a cache that evicts at random.
If you must use WeakHashMap for caching, pair it with a reference queue monitor and log eviction events in development to understand the GC pattern. In production, profile heap usage before and after. You'll likely find that the "free" cache costs you more in tuning than it saves in memory.
Overview
WeakHashMap in Java is a Map implementation that stores keys using weak references. This means that when a key is no longer referenced from anywhere else in the application, the garbage collector (GC) can reclaim both the key and its associated entry from the map automatically. Unlike HashMap, WeakHashMap does not prevent its keys from being garbage collected, making it ideal for scenarios where you want to associate metadata with objects without preventing their cleanup. Built on top of the ReferenceQueue mechanism, WeakHashMap checks for collected keys during every get(), put(), or size() operation and removes stale entries lazily. This design solves the problem of memory leaks in caches and listener registries, but introduces unpredictable eviction timing since GC runs nondeterministically. Understanding when and how WeakHashMap works — and when it doesn't — is critical for building memory-safe applications.
System.gc() is just a suggestion — GC may not run immediately, so WeakHashMap eviction is nondeterministic and should never be relied upon for timing-sensitive logic.Conclusion
WeakHashMap is a powerful but often misunderstood tool in Java's collection framework. Its true strength lies in eliminating accidental memory leaks when keys have a limited lifecycle tied to external references — such as in caches for classloaders, temporary metadata maps, or listener registries. However, it is not a general-purpose cache alternative because eviction is tied to GC behavior, not time or memory pressure. Developers must weigh the tradeoff between automatic cleanup and nondeterministic eviction. For high-performance or predictable caching, consider using Guava's Cache or Caffeine instead. The key takeaway: use WeakHashMap only when you need automatic entry removal based on key reachability, and never rely on it for critical memory management. Proper use of constructors — with custom initial capacity, load factor, or populating from an existing Map — allows fine-tuning performance. Understanding the GC trinity of strong, soft, and weak references is non-negotiable before adopting WeakHashMap in production.
Cache-Driven OutOfMemoryError in a Session Store
- WeakHashMap only evicts when keys have zero strong references — not when they 'expire'.
- Always audit all strong references to keys before relying on WeakHashMap for eviction.
- If you need time-based eviction, use a proper cache like Caffeine or Ehcache.
queue.poll() in operations — if no map operations occur after GC, stale entries linger. Force a map.size() or get() to trigger cleanup.jcmd <pid> GC.runjmap -histo:live <pid> | grep YourKeyClassmap.size() every minute to force cleanup.Key takeaways
size() after nullifying keys to trigger it.Common mistakes to avoid
3 patternsWeakHashMap clears stale keys automatically — no extra care needed
Using WeakHashMap for all caching scenarios
Assuming map operations are always O(1) regardless of staleness
map.size() periodically in a background thread to spread cleanup cost.Interview Questions on This Topic
Explain how WeakHashMap prevents memory leaks and what its limitations are.
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
That's Collections. Mark it forged?
8 min read · try the examples if you haven't