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 aRunnable
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