John
John

Reputation: 31

Java - Check if reference to object in Map exists

A few weeks back I wrote a Java class with the following behavior:

As code:

public class MyClass
{
    private static Map<Integer, MyClass> map;
    private final int field;

    static
    {
        map = new HashMap<>();
    }

    private MyClass(int field)
    {
        this.field = field;
    }

    public static MyClass get(int field)
    {
        synchronized (map)
        {
            return map.computeIfAbsent(field, MyClass::new);
        }
    }
}

This way I can be sure, that only one object exists for each integer (as field). I'm currently concerned, that this will prevent the GC to collect objects, which I no longer need, since the objects are always stored in the map (a reference exists)...

If I wrote a loop like function like this:

public void myFunction() {
    for (int i = 0; i < Integer.MAX_VALUE; i++) {
       MyClass c = MyClass.get(i);
       // DO STUFF
    } 
}

I would end up with Integer.MAX_VALUE objects in memory after calling the method. Is there a way I can check, whether references to objects in the map exists and otherwise remove them?

Upvotes: 3

Views: 1923

Answers (2)

errantlinguist
errantlinguist

Reputation: 3818

This looks like a typical case of the multiton pattern: You want to have at most one instance of MyClass for a given key. However, you also seem to want to limit the amount of instances created. This is very easy to do by lazily instantiating your MyClass instances as you need them. Additionally, you want to clean up unused instances:

Is there a way I can check, whether references to objects in the map exists and otherwise remove them?

This is exactly what the JVM's garbage collector is for; There is no reason to try to implement your own form of "garbage collection" when the Java core library already provides tools for marking certain references as "not strong", i.e. should refer to a given object only if there is a strong reference (i.e. in Java, a "normal" reference) somewhere referring to it.

Implementation using Reference objects

Instead of a Map<Integer, MyClass>, you should use a Map<Integer, WeakReference<MyClass>> or a Map<Integer, SoftReference<MyClass>>: Both WeakReference and SoftReference allow the MyClass instances they refer to to be garbage-collected if there are no strong (read: "normal") references to the object. The difference between the two is that the former releases the reference on the next garbage collection action after all strong references are gone, while the latter one only releases the reference when it "has to", i.e. at some point which is convenient for the JVM (see related SO question).

Plus, there is no need to synchronize your entire Map: You can simply use a ConcurrentHashMap (which implements ConcurrentMap), which handles multi-threading in a way much better than by locking all access to the entire map. Therefore, your MyClass.get(int) could look like this:

private static final ConcurrentMap<Integer, Reference<MyClass>> INSTANCES = new ConcurrentHashMap<>();

public static MyClass get(final int field) {
    // ConcurrentHashMap.compute(...) is atomic <https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentHashMap.html#compute-K-java.util.function.BiFunction->
    final Reference<MyClass> ref = INSTANCES.compute(field, (key, oldValue) -> {
        final Reference<MyClass> newValue;
        if (oldValue == null) {
            // No instance has yet been created; Create one
            newValue = new SoftReference<>(new MyClass(key));
        } else if (oldValue.get() == null) {
            // The old instance has already been deleted; Replace it with a
            // new reference to a new instance
            newValue = new SoftReference<>(new MyClass(key));
        } else {
            // The existing instance has not yet been deleted; Re-use it
            newValue = oldValue;
        }
        return newValue;
    });
    return ref.get();
}

Finally, in a comment above, you mentioned that you would "prefer to cache maybe up to say 1000 objects and after that only cache, what is currently required/referenced". Although I personally see little (good) reason for it, it is possible to perform eager instantiation on the "first" 1000 objects by adding them to the INSTANCES map on creation:

private static final ConcurrentMap<Integer, Reference<MyClass>> INSTANCES = createInstanceMap();

private static ConcurrentMap<Integer, Reference<MyClass>> createInstanceMap() {
    // The set of keys to eagerly initialize instances for
    final Stream<Integer> keys = IntStream.range(0, 1000).boxed();
    final Collector<Integer, ?, ConcurrentMap<Integer, Reference<MyClass>>> mapFactory = Collectors
            .toConcurrentMap(Function.identity(), key -> new SoftReference<>(new MyClass(key)));
    return keys.collect(mapFactory);
}

How you define which objects are the "first" ones is up to you; Here, I'm just using the natural order of the integer keys because it's suitable for a simple example.

Upvotes: 3

AJNeufeld
AJNeufeld

Reputation: 8695

Your function for examining your cache is cringe worthy. First, as you said, it creates all the cache objects. Second, it iterates Integer.MAX_VALUE times.

Better would be:

public void myFunction() {
    for(MyClass c : map.values()) {
       // DO STUFF
    } 
}

To the issue at hand: Is it possible to find out whether an Object has references to it?

Yes. It is possible. But you won't like it.

http://docs.oracle.com/javase/1.5.0/docs/guide/jvmti/jvmti.html

jvmtiError
IterateOverReachableObjects(jvmtiEnv* env,
        jvmtiHeapRootCallback heap_root_callback,
        jvmtiStackReferenceCallback stack_ref_callback,
        jvmtiObjectReferenceCallback object_ref_callback,
        void* user_data)

Loop over all reachable objects in the heap. If a MyClass object is reachable, then, well, it is reachable.

Of course, by storing the object in your cache, you are making it reachable, so you'd have to change your cache to WeakReferences, and see if you can exclude those from the iteration.

And you're no longer using pure Java, and jvmti may not be supported by all VM's.

As I said, you won't like it.

Upvotes: 1

Related Questions