Problem in NativeObjectManager

For the longest time during development of my game I have had a very rare “NativeObject is not registered in this NativeObjectManager” crash. It was uncommon enough that I was mostly ignoring it. I was certainly never able to get it to be reproducible. Last week however I found a user for whom the problem was reproducible, so I got my code sleuthing hat on.

It took a while to figure out what the NativeObjectManager actually does (always room for more learning, right?) but the result of my investigation is that very, very rarely a NativeObject that has been queued for deletion by the user via .dispose() will - a few frames later - get garbage collected before the now-dead Reference that was removed earlier and will therefore end up in NativeObjectManager’s ReferenceQueue where upon there will be a second attempt to delete it, resulting in the aforementioned exception.

I don’t know quite why this situation occurs, but as I was researching the issue I came across the Cleaner class which was introduced in Java 9 and postdates the NativeObjectManager, but performs essentially the same function. Lo and behold its description contains the advice:

“The most efficient use is to explicitly invoke the clean method when the object is closed or no longer needed. The cleaning action is a Runnable to be invoked at most once when the object has become phantom reachable unless it has already been explicitly cleaned.”

OpenJDK’s PhantomCleanable implementation supports this too:

/**
* Unregister this PhantomCleanable and invoke {@link #performCleanup()},
* ensuring at-most-once semantics.
*/
@Override
public final void clean() {
    if (remove()) {
        super.clear();
        performCleanup();
    }
}

Armed with this knowledge I threw in an explicit call to .clear() in order to make sure the Reference in question doesn’t appear in the queue when it gets GCed. This appears to have fixed the problem:

// Unregister it from cleanup list.
NativeObjectRef ref2 = refMap.remove(obj.getUniqueId());
if (ref2 == null) {
    throw new IllegalArgumentException("The " + obj + " NativeObject is not "
                            + "registered in this NativeObjectManager");
}

assert ref == null || ref == ref2;
                
ref2.clear();

As a side note, the double deletion warning:

if (obj.getId() <= 0) {
    logger.log(Level.WARNING, "Object already deleted: {0}", obj.getClass().getSimpleName() + "/" + obj.getId());
}

seems to give a false sense of security since in this case of double deletion, one comes from the user, which invalidates the ID of the original object, and the other comes from the ReferenceQueue, which invalidates the ID of the destructible clone.

Hopefully this is of some value and not just nonsense :slight_smile:

2 Likes

So in your case you are manually disposing native objects?

…trying to figure out why I never hit this issue.

1 Like

Yes, I am manually disposing of the bulk of the NativeObjects I create.

1 Like

Note: while we should definitely fix the issues… you also probably don’t need to do that.

1 Like