Is WeakHashMap ever-growing, or does it clear out the garbage keys?
Asked Answered
P

2

4

I am trying to use WeakHashMap as a concurrent Set of weak references.

    this.subscribers =
            Collections.synchronizedSet(
                    Collections.newSetFromMap(
                            new WeakHashMap <>()
                    )
            );

When an element goes to garbage-collection, my set continues to report it as a part of the collection. So it seems the map is ever-growing.

The documentation says:

When a key has been discarded its entry is effectively removed from the map,…

But that does not seem to be the case in practice.

Is there ever a point at which the WeakHashMap clears out the detritus?

Paget answered 13/10, 2018 at 5:30 Comment(6)
"When an element goes to garbage-collection, my set continues to report it as a part of the collection" - how are you determining this?Kirksey
@Kirksey I am calling Set::size().Paget
"This result is a snapshot, and may not reflect unprocessed entries that will be removed before next attempted access because they are no longer referenced."Kirksey
Also, how are you determining that those objects have been garbage collected?Kirksey
@Kirksey Indeed I must have been wrong in assuming the objects were garbage-collected. See my posted Answer with a small experiment showing that the Set reports it size declining whether due to calling Set::remove or due to becoming garbage. I will have to explore my original code problem more thoroughly.Paget
Related: Why WeakHashMap holds strong reference to value after GC?Paget
P
4

Yes, keys cleared after garbage is actually collected

Yes, WeakHashMap does clean out the detritus. The keys that have gone to garbage collection are no longer reported in the size. But you must wait for garbage-collection to actually take place.

Seems likely that you were incorrect about your objects going to garbage-collection. Perhaps your objects became candidates for garbage-collection, but have not yet been collected. Try invoking the garbage-collector and waiting a moment for it to complete. But remember, the call to System.gc() is only a suggestion to the JVM and may be ignored depending on your JVM implementation and current runtime scenario.

Here is a complete example app. Notice that the Set reports a decrease in size whether calling Set::remove or letting the object go out of scope.

package com.basilbourque.example;

import java.util.Collections;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;

public class WeakHashMapExercise {

    public static void main ( String[] args ) {
        WeakHashMapExercise app = new WeakHashMapExercise();
        app.doIt();
    }

    private void doIt ( ) {
        Set < UUID > set =
                Collections.synchronizedSet(
                        Collections.newSetFromMap(
                                new WeakHashMap <>()
                        )
                );

        UUID uuid1 = UUID.fromString( "a8ee1e34-cead-11e8-a8d5-f2801f1b9fd1" );
        UUID uuid2 = UUID.fromString( "39bda2b4-5885-4f56-a900-411a49beebac" );
        UUID uuid3 = UUID.fromString( "0b630385-0452-4b96-9238-20cdce37cf55" );
        UUID uuid4 = UUID.fromString( "98d2bacf-3f7f-4ea0-9c17-c91f6702322c" );

        System.out.println( "Size before adding: " + set.size() );

        set.add( uuid1 );
        set.add( uuid2 );
        set.add( uuid3 );
        set.add( uuid4 );

        System.out.println( "Size after adding 4 items: " + set.size() );  // Expect 4.

        set.remove( uuid3 );

        System.out.println( "Size after removing item # 3: " + set.size() );  // Expect 3.

        uuid2 = null;  // Release that UUID to garbage-collection.

        // That released object may still appear in our `Set` until garbage collection actually executes. 
        System.gc(); // Ask the JVM to run the garbage-collection. Only a suggestion, may be ignored.
        try {
            Thread.sleep( 1_000 );  // Wait a moment, just for the heck of it.
        } catch ( InterruptedException e ) {
            e.printStackTrace();
        }

        System.out.println( "Size after making garbage of item # 2: " + set.size() );  // Expect 2.

        for ( UUID uuid : set ) {
            System.out.println( uuid.toString() );
        }


    }
}

See this code run live at IdeOne.com.

Size before adding: 0

Size after adding 4 items: 4

Size after removing item # 3: 3

Size after making garbage of item # 2: 2

In my case, using Java 10.0.2 version of OpenJDK-based Zulu JVM from Azul Systems, the garbage collector does seem to be activating upon my request. If I comment out the delay for a second, or the System.gc call, then the last size reported remains 3 rather than the expected 2.

You can even see this behavior when running this code live at IdeOne.com. Notice how the last item below is 3 but above is 2.

Size before adding: 0

Size after adding 4 items: 4

Size after removing item # 3: 3

Size after making garbage of item # 2: 3

Paget answered 13/10, 2018 at 6:16 Comment(3)
Yes, keys cleared after garbage is actually collected - is not entirely correct. When GC clears a weak reference, it posts an "event" to a reference queue - that process is asynchronous, and even if GC has "cleared" your keys, WeakHashMap still has a strong reference to the value. The actual clean-up happens when 1) GC has posted the event to the reference queue (you have no control of when this happens) 2) you call any other method on the WeakHashMap - that will do the needed clean-up.Circumjacent
@Circumjacent Thanks for the details. Can you cite a source for this info, for further reading?Paget
I've posted an answer with some details and comments, should be obvious what is going on IMO...Circumjacent
C
2

When garbage-collection clears a weak reference, it posts an "event" to a reference queue. That process is asynchronous, and even if GC has "cleared" your keys, WeakHashMap still has a strong reference to the value. The actual clean-up happens when:

  1. Garbage Collector has posted the event to the reference queue (you have no control of when this happens).
  2. You call any other method on the WeakHashMap - that will do the needed clean-up.

Here is an example to show what is going on.

public class WeakHashMapInAction {

    public static void main(String[] args) {

        Key key = new Key();
        KeyMetadata keyMeta = new KeyMetadata("keyMeta");

        WeakHashMap<Key, KeyMetadata> map = new WeakHashMap<>();
        map.put(key, keyMeta);

        // wrap the key into a weakReference
        WeakReference<Key> keyReference = new WeakReference<>(key);

        // force key to be GC-ed
        key = null;
        for (; keyReference.get() != null; ) {
            System.gc();
        }

        // at this point keyReference::get returns null,
        // meaning the GC has reclaimed "key";
        // that does NOT mean WeakHashMap removed that entry though

        // you can enable this code to see that "not yet collected" is not printed at all
        // since you are giving enough time for the Reference thread to post to that ReferenceQueue
        // LockSupport.parkNanos(10000000);

        while (map.size() == 1) {
            // if you run this enough times, you will see this sometimes is printed
            // even if WeakHashMap::size calls "expungeStaleEntries" internally
            // it does not mean that the event to the queue was pushed in time
            // by the Reference thread
            System.out.println("not yet collected");
        }

        System.out.println("collected");

    }


    static class Key {

    }

    @RequiredArgsConstructor
    @Getter
    static class KeyMetadata {
        private final String someInfo;

        // Constructor.
        KeyMetadata ( String someInfo ) { this.someInfo = someInfo; }
    }

}
Circumjacent answered 10/9, 2019 at 18:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.