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:

    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.