Search code examples
javagarbage-collectionguavaweakhashmap

Why WeakHashMap holds strong reference to value after GC?


Key object in WeakHashMap became weakly reachable. And map should be remove the entry after GC. But a strong reference to the value object remains. Why?

The same behavior is observed with guava weakkeys map.

Expected output:

...
refKey.get = null
refValue.get = null

But I get output:

map.keys = []
map.values = []
map.size = 0
refKey.get = null
refValue.get = (123)

Code:

import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.WeakHashMap;
import com.google.common.collect.MapMaker;

public class Test {

    static class Number {
        final int number;
        public Number(int number) { this.number = number; }
        public String toString() { return "(" + number + ")"; }
    }

    static class Key extends Number {
        public Key(int number) { super(number); }
    }

    static class Value extends Number {
        public Value(int number) { super(number); }
    }

    public static void main(String args[]) {

        //Map<Key, Value> map = new MapMaker().weakKeys().makeMap();
        Map<Key, Value> map = new WeakHashMap<>();

        Key key = new Key(1);
        Value value = new Value(123);

        map.put(key, value);

        WeakReference<Key> refKey = new WeakReference<>(key);
        WeakReference<Value> refValue = new WeakReference<>(value);

        key = null;
        value = null;

        System.gc();

        System.out.println("map.keys = " + map.keySet());
        System.out.println("map.values = " + map.values());
        System.out.println("map.size = " + map.size());
        System.out.println("refKey.get = " + refKey.get());
        System.out.println("refValue.get = " + refValue.get());

    }

}

UPD:

I tried perform GC in jСonsole and jcmd but output was not changed.


Solution

  • The WeakHashMap contains Map.Entry instances which reference the key using a WeakReference (actually, in OpenJDK / Oracle JDK, it directly extends WeakReference).

    When the GC happens, the entries which now reference absent keys are not magically removed from the map: they're still present until cleared, which is why the value is also still present and has not been collected yet.

    In OpenJDK, that happens in expungeStaleEntries() using a ReferenceQueue, and that method is called from a number of places:

    • size()
    • resize()
    • getTable() which is itself called from multiple methods, including get() and put()

    If you want your value to be garbage collectable, you should interact with the WeakHashMap, e.g. by asking for its size() or doing a lookup.

    Note that it means the value cannot be collected until a second garbage collection.

    If I remember correctly, it works more or less the same way in Guava.