The class WeakHashMap from java.util is a hash-based Map that holds its keys through weak references, so the garbage collector can remove an entry once nothing else in the app refers to the key. The values stay strongly referenced, but they disappear together with their keys.
We use a WeakHashMap to attach extra data to objects that some other part of the app owns, such as a label per session object or a computed summary per document, without keeping those objects alive after their owner drops them.
The following example stores a label for a session object, drops the only strong reference to the session and asks the JVM to run a garbage collection.
Map<Object, String> labels = new WeakHashMap<>();
Object session = new Object();
labels.put(session, "lokesh");
String label = labels.get(session); // "lokesh"
int before = labels.size(); // 1
session = null; // the key is no longer strongly reachable
System.gc(); // only a request to the JVM
int after = labels.size(); // 0 once the GC has cleared the key, else still 1
Notice the comment on the last line. The map loses the entry only after the garbage collector has cleared the key, and our code cannot control when that happens. We cover the reference types behind this behavior, the methods, the traps with String and Integer keys, a comparison with HashMap and a real-world use.
1. Weak Keys in a WeakHashMap
A normal variable holds a strong reference, and the garbage collector never removes an object that is still strongly reachable. A WeakHashMap wraps each key in a WeakReference inside the map, so the map itself does not count as a reason to keep the key alive. When the last strong reference to a key goes away, the collector clears the weak reference and the map drops the entry.

The map does not watch the collector in the background. The collector puts each cleared reference on an internal ReferenceQueue, and the map removes those stale entries the next time we call a method such as get(), put() or size().
Java has four reference strengths, and two of them matter for maps. The difference between them decides when an object becomes collectable.
| Reference type | Created with | When the GC may clear it |
|---|---|---|
| Strong | Object o = new Object(); | Never, while the reference exists |
| Soft | new SoftReference<>(o) | When memory runs low, and always before an OutOfMemoryError |
| Weak | new WeakReference<>(o) | At the next collection that finds no strong or soft reference to the object |
| Phantom | new PhantomReference<>(o, queue) | After the object becomes unreachable, used for cleanup with Cleaner |
A WeakHashMap uses the weak kind, so it does not wait for memory pressure. The garbage collection algorithms decide how often collections run and therefore how soon a cleared key leaves the map.
2. Creating a WeakHashMap and Calling Its Methods
The class has the same constructors as HashMap. By default it starts with 16 buckets and a load factor of 0.75. Since Java 19, the static factory newWeakHashMap(int) creates a map sized for a given number of mappings, so we do not have to calculate the capacity from the load factor.
Map<String, Integer> empty = new WeakHashMap<>(); // 16 buckets, load factor 0.75
Map<String, Integer> sized = new WeakHashMap<>(64); // 64 buckets
Map<String, Integer> forHundred = WeakHashMap.newWeakHashMap(100); // room for 100 mappings without resizing
Map<String, Integer> copy = new WeakHashMap<>(Map.of("apple", 5)); // copies the entries
All the usual Map methods work, including the Java 8 defaults such as getOrDefault(), merge() and computeIfAbsent(). The keys in the next snippet are String literals, which the JVM keeps in its string pool, so these entries never disappear on their own, as we explain in section 4.
Map<String, Integer> stock = new WeakHashMap<>();
stock.put("apple", 5);
stock.put("banana", 3);
stock.merge("apple", 2, Integer::sum);
Integer apples = stock.get("apple"); // 7
Integer pears = stock.getOrDefault("pear", 0); // 0
boolean hasBanana = stock.containsKey("banana"); // true
boolean hasNine = stock.containsValue(9); // false
Integer removed = stock.remove("banana"); // 3
int size = stock.size(); // 1
Iteration works through keySet(), values() and entrySet(), and the iterators are fail-fast. Like a HashMap, a WeakHashMap has no defined iteration order, and it allows one null key and any number of null values.
3. When Does an Entry Disappear?
An entry becomes removable when its key is only weakly reachable, which means no local variable, field, collection or static reference outside the map points to the key. From that moment, the timing belongs to the JVM. The collector clears the reference at some later collection, and the map removes the entry on its next method call.
The call System.gc() is only a request, so a program must never depend on an entry being gone at a certain line. The JVM can ignore the request, for example when it runs with -XX:+DisableExplicitGC. The following example keeps one key reachable and drops the other.
Map<Object, String> owners = new WeakHashMap<>();
Object kept = new Object();
Object dropped = new Object();
owners.put(kept, "alex");
owners.put(dropped, "bob");
dropped = null; // only the map refers to the second key
System.gc();
boolean keptPresent = owners.containsKey(kept); // true, kept is still strongly reachable
int remaining = owners.size(); // 1 after the GC clears the dropped key, 2 before
A WeakHashMap behaves as if an unknown thread were removing entries. Even with no writes from our code, size() can return smaller values over time, and get() can return a value once and null later for a key that we created again with equal content.
In tests, we either poll with a timeout or check only the guaranteed part, which is that a strongly reachable key stays in the map. A test that calls System.gc() once and asserts an exact size fails on some machines and passes on others.
4. Keys That Never Leave the Map
Some keys stay strongly reachable from places our code does not see, and their entries stay forever. Other entries stay because the map itself keeps the key alive through the value.
- String literals and compile-time constants live in the string pool, so map.put(“apple”, 5) creates an entry that the collector never removes.
- Boxed values from Integer.valueOf() between -128 and 127 come from a shared cache, so map.put(10, “x”) uses a key that is always reachable.
- Enum constants and Class objects of loaded classes are reachable for the life of the class loader.
- A value that refers to its own key, either itself or through other objects, keeps that key strongly reachable through the map.
The last case is a real memory leak, because the entry looks removable but never goes away. The following example stores a value that holds its key.
record Profile(Object owner, String name) {}
Map<Object, Profile> leaking = new WeakHashMap<>();
Object user = new Object();
leaking.put(user, new Profile(user, "lokesh")); // the value points back to the key
user = null;
System.gc();
int leaked = leaking.size(); // stays 1, the value keeps the key alive
The fix is to keep the back reference out of the value, or to wrap the value in a WeakReference when nothing else needs to keep the value alive. With the wrapper, each get() returns the reference, and we call get() on it to read the value.
Map<Object, WeakReference<Profile>> safe = new WeakHashMap<>();
Object owner = new Object();
Profile profile = new Profile(owner, "lokesh");
safe.put(owner, new WeakReference<>(profile));
WeakReference<Profile> ref = safe.get(owner);
Profile found = ref == null ? null : ref.get();
String name = found != null ? found.name() : "unknown"; // "lokesh"
Lookups still use equals() and hashCode(), not identity. A String key built at runtime, such as new String(“report”), can disappear while code later looks it up with an equal string and gets null, which confuses readers of the logs. Keys with identity-based equals(), such as session or connection objects, match the weak-key model best.
5. WeakHashMap vs HashMap
Both classes use a hash table with buckets, and both have the same API, the same null support and the same lack of thread safety. The difference is who decides when an entry leaves the map.
| Feature | HashMap | WeakHashMap |
|---|---|---|
| Key reference | Strong | Weak |
| Entry removal | Only through our calls, such as remove() | Also by the GC, once the key is only weakly reachable |
| size() without writes | Stays the same | Can shrink over time |
| Good keys | Immutable value objects | Objects with identity, owned elsewhere |
| Cloneable | Yes | No |
| Typical use | General key-value storage | Metadata attached to objects owned by other code |
A HashMap that maps objects to metadata keeps every object alive until we remove the entry, which is a common source of leaks in long-running servers. The HashMap internals are similar in both classes, so the lookup speed is similar too.
6. Attaching Data to Objects We Do Not Own
Say a document editor creates a Document object for every open tab, and a plugin computes a word count for each document. The plugin cannot add a field to Document, and it gets no event when a tab closes. If the plugin stored the counts in a HashMap, every closed document would stay in memory for as long as the editor runs.
static final class Document {
private final String text;
Document(String text) { this.text = text; }
String text() { return text; }
}
static int wordCount(Document doc) {
return WORD_COUNTS.computeIfAbsent(doc, d -> d.text().split("\\s+").length);
}
static final Map<Document, Integer> WORD_COUNTS =
Collections.synchronizedMap(new WeakHashMap<>());
Document tab = new Document("weak keys are cleared by the collector");
int words = wordCount(tab); // 7
int cached = WORD_COUNTS.size(); // 1, until the editor drops the tab
The Document class keeps the identity-based equals() from Object, so two tabs with the same text get separate counts. When the editor closes the tab and drops its Document, the count becomes collectable with it. The wrapper from Collections.synchronizedMap() makes the map safe for the editor’s worker threads.
For memory-sensitive caches, a WeakHashMap is the wrong tool, because entries vanish at the next collection even when memory is plentiful. A cache library such as Caffeine offers size limits, expiry, and weak or soft references in one place. For data per Class object, the JDK class ClassValue avoids class loader leaks.
7. WeakHashMap FAQs
Weak references raise a few practical questions once the map runs in a real app.
7.1. Is WeakHashMap thread-safe?
No. Its methods are not synchronized, and the GC removes entries while other threads may read the map. We wrap it with Collections.synchronizedMap(), as in section 6, or use a concurrent cache library with weak keys, because the JDK has no concurrent weak map. See the ConcurrentMap guide for the concurrent maps in java.util.concurrent.
7.2. Why is an entry still in the map after System.gc()?
Either the key is still strongly reachable, or the JVM has not cleared it yet. Check for String literals, cached Integer values, values that point back to their key and other collections that hold the key. When none of these apply, the entry goes away at a later collection.
7.3. Can we use WeakHashMap as a cache?
Only for metadata that must live as long as the key object and no longer. A general cache needs eviction based on size, time or memory, which WeakHashMap does not offer. Entries keyed by String literals never leave, and entries keyed by short-lived objects leave at the first collection after the last strong reference is gone.
7.4. Does WeakHashMap allow null keys and values?
Yes. It stores the null key with an internal placeholder object that is always reachable, so a null key stays in the map until we remove it. Values can be null as well.
7.5. What is the difference between WeakHashMap and IdentityHashMap?
A WeakHashMap changes when entries are removed, whereas an IdentityHashMap changes how keys are compared, using == instead of equals(). Neither class has the behavior of the other.
8. Conclusion
A WeakHashMap holds its keys through weak references, so an entry leaves the map after the last strong reference to its key is gone and the collector has cleared it. The timing is up to the JVM, which makes System.gc() a request and the size of the map a moving number.
The map fits metadata attached to objects that other code owns. Keys from the string pool or the Integer cache never leave, values must not point back to their keys, and caches and concurrent access need a cache library or a synchronized wrapper. For the other map types, see our overview of Java Collections.
9. References
- WeakHashMap Javadoc (Java 25)
- java.lang.ref package (reachability)
- System.gc() Javadoc
- ClassValue Javadoc
Happy Learning !!