Is this an appropriate list to discuss JNI?

I'm concerned that the current semantics of JNI WeakGlobalRefs are still
dangerous in a very subtle way that is hidden in the spec. The current
(14+) spec says:

“Weak global references are related to Java phantom references
(java.lang.ref.PhantomReference). A weak global reference to a specific
object is treated as a phantom reference referring to that object when
determining whether the object is phantom reachable (see java.lang.ref).
---> Such a weak global reference will become functionally equivalent to
NULL at the same time as a PhantomReference referring to that same object
would be cleared by the garbage collector. <---”

(This was the result of JDK-8220617, and is IMO a large improvement over
the prior version, but ...)

Consider what happens if I have a WeakGlobalRef W that refers to a Java
object A which, possibly indirectly, relies on an object F, where F is
finalizable, i.e.

W - - -> A -----> ... -----> F

Assume that F becomes invalid once it is finalized, e.g. because the
finalizer deallocates a native object that F relies on. This seems to be a
very common case. We are then exposed to the following scenario:

0) At some point, there are no longer any other references to A or F.
1) F is enqueued for finalization.
2) W is dereferenced by Thread 1, yielding a strong reference to A and
transitively to F.
3) F is finalized.
4) Thread 1 uses A and F, accessing F, which is no longer valid.
5) Crash, or possibly memory corruption followed by a later crash elsewhere.

(3) and (4) actually race, so there is some synchronization effort and cost
required to prevent F from corrupting memory. Commonly the implementer of W
will have no idea that F even exists.

I believe that typically there is no way to prevent this scenario, unless
the developer adding W actually knows how every class that A could possibly
rely on, including those in the Java standard library, are implemented.

This is reminiscent of finalizer ordering issues. But it seems to be worse,
in that there isn't even a semi-plausible workaround.

I believe all of this is exactly the reason PhantomReference.get() always
returns null, while WeakReference provides significantly different
semantics, and WeakReferences are enqueued when an object is enqueued for
finalization.

The situation improves, but the problem doesn't fully disappear, in a
hypothetical world without finalizers. It's still possible to use
WeakGlobalRef to get a strong reference to A after a WeakReference to A has
been cleared and enqueued. I think the problem does go away if all cleanup
code were to use PhantomReference-based Cleaners.

AFAICT, backward-compatibility aside, the obvious solution here is to have
WeakGlobalRefs behave like WeakReferences. My impression is that this would
fix significantly more broken clients than it would break correct ones, so
it is arguably still a viable option.

There is a case in which the current semantics are actually the desired
ones, namely when implementing, say, a String intern table. In this case
it's important the reference not be cleared even if the referent is, at
some point, only reachable via a finalizer. But this use case again relies
on the programmer knowing that no part of the referent is invalidated by a
finalizer. That's a reasonable assumption for the
Java-implementation-provided String intern table. But I'm not sure it's
reasonable for any user-written code.

There seem to be two ways forward here:

1) Make WeakGlobalRefs behave like WeakReferences instead of
PhantomReferences, or
2) Add strong warnings to the spec that basically suggest using a strong
GlobalRef to a WeakReference instead.

Has there been prior discussion of this? Are there reasonable use cases for
the current semantics? Is there something else that I'm overlooking? If
not, what's the best way forward here?

(I found some discussion from JDK-8220617, including a message I posted.
Unfortunately, it seems to me that all of us overlooked this issue?)

Hans

Reply via email to