Why WeakHashMap holds strong reference to value after GC?
Asked Answered
S

3

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.

Skyeskyhigh answered 19/1, 2016 at 13:32 Comment(7)
@Thilo In this case I would get output: refKey.get() = (1). But refKey.get() = null.Skyeskyhigh
But that is an output you could get when the garbage collection is only half done, no?Quadrillion
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).Quadrillion
@Quadrillion Thanks for the advice. I tried jConsole and jcmd but output was not changed.Skyeskyhigh
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.Blanketing
@Quadrillion Tried for hundred keys and values. All keys was collected but all values - not.Skyeskyhigh
Related: #9167110Quadrillion
O
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.

Openhanded answered 19/1, 2016 at 14:28 Comment(3)
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).Quadrillion
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.Quadrillion
@Quadrillion 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.Openhanded
I
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.

Igniter answered 19/1, 2016 at 14:18 Comment(1)
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).Quadrillion
E
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

Exuberate answered 19/1, 2016 at 13:38 Comment(2)
This does not answer the question.Reggie
n jdk 1.7 onwards you can use below command to force gc to run jcmd <pid> GC.run You can get process id using jps.Exuberate

© 2022 - 2024 — McMap. All rights reserved.