7

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.

Rumter
  • 73
  • 4
  • @Thilo In this case I would get output: `refKey.get() = (1)`. But `refKey.get() = null`. – Rumter Jan 19 '16 at 13:41
  • But that is an output you could get when the garbage collection is only half done, no? – Thilo Jan 19 '16 at 13:49
  • Did you try jconsole or jcmd to see if that changes the output (you'll have to sleep long enough for you to fit that in). – Thilo Jan 19 '16 at 13:53
  • @Thilo Thanks for the advice. I tried jConsole and jcmd but output was not changed. – Rumter Jan 19 '16 at 14:03
  • A GC doesn't guarantee to clean up all Weak References however I would have expected the object to be either cleaned up everywhere, or not at all. – Peter Lawrey Jan 19 '16 at 14:03
  • 1
    @Thilo Tried for hundred keys and values. All keys was collected but all values - not. – Rumter Jan 19 '16 at 14:20
  • Related: http://stackoverflow.com/questions/9166610/weakhashmap-and-strongly-referenced-value?rq=1 – Thilo Jan 19 '16 at 14:33

3 Answers3

7

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.

Frank Pavageau
  • 11,477
  • 1
  • 43
  • 53
  • 1
    This is kind of lame ... Should have been mentioned in the JavaDocs (similar to that caveat they have about indirectly referencing the key from the value). – Thilo Jan 19 '16 at 14:36
  • Re: "lame". OTOH, it's probably not an issue at all in real life, as the purge happens whenever you do pretty much anything with the map. Still... could have been mentioned in the Javadocs. – Thilo Jan 19 '16 at 14:41
  • 1
    @Thilo Yes, it could be more explicit in the Javadoc for people unfamiliar with the garbage collection process. If you're familiar with it, you know the entries can't magically disappear without some interaction with the map. It's slightly more explicit in `MapMaker`'s Javadoc, though not totally either. – Frank Pavageau Jan 19 '16 at 14:56
2

Running this modification of the code and forcing GC in Jconsole confirms removal of the reference.

    System.gc();

    try {
        while (refValue.get() != null) {
            System.out.println("map.keys = " + map.keySet());
            Thread.sleep(5000);
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    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());

For some reason though, if map.keySet() is not queried in the loop, it never terminates, that is refValue does not become null.

nolexa
  • 2,392
  • 2
  • 19
  • 19
  • 2
    If you look at the source for WeakHashMap, there is a method `expungeStaleEntries` that gets called when you ask for things like `size` or `keySet`. Presumably that gets rid of the reference to the value (and until then, there is a hard reference to it). – Thilo Jan 19 '16 at 14:24
1

You have no control over GC in java. the VM decides. I've never run across a case where System.gc() is needed. Since a System.gc() call simply SUGGESTS that the VM do a garbage collection and it also does a FULL garbage collection (old and new generations in a multi-generational heap), then it can actually cause MORE cpu cycles to be consumed than necessary.

In jdk 1.7 onwards you can use below command to force gc to run

jcmd <pid> GC.run

Get processid using jps

Pankaj Pandey
  • 1,015
  • 6
  • 10