JDK-6721588 : Server JIT optimization can cause objects to go out of scope prematurely
  • Type: Bug
  • Status: Closed
  • Resolution: Not an Issue
  • Component: hotspot
  • Sub-Component: compiler
  • Priority: P3
  • Affected Version: 5.0u15
  • OS: solaris_10
  • CPU: x86
  • Submit Date: 2008-07-02
  • Updated Date: 2010-04-02
  • Resolved Date: 2008-08-26
Solaris 10, AMD64

Java HotSpot(TM) 64-Bit Server VM (build 1.5.0_15-b04, mixed mode)

DESCRIPTION from Licensee:
We have encountered a situation where objects can go out of scope unexpectedly. In our case this leads to crash due a SIGSEGV in our JNI code, because a native structure is cleared away by finalisation before the native code has finished with it.

Consider the following Java code:

  public void doStuff() {
    NativeThing nt = new NativeThing();

In our case, the constructor for NativeThing creates a new native structure, and stores a pointer to that structure in a long field.

The evaluateNativeThing() method takes the pointer from the NativeThing instance's long field and passes it to some native code via JNI, which then does some work on the native structure.

The NativeThing class implementation also contains a finalize() method to clear away the native structure associated with it prior to garbage collection.

Occasionally, we see a SIGSEGV in the native evaluateNativeThing() code because the native stucture has been cleared away before we've finished with it.

This should not be able to happen - the NativeThing instance should remain in scope until doStuff() returns. However, it seems that the NativeThing instance can actually go out of scope and be eligible for finalization before even the evaluateNativeThing() method has returned.

We suspect that this is happening due to a Server JIT optimization based on the fact that the NativeThing instance *effectively* goes out of scope as soon as evaluateNativeThing() is called. This optimization would not cause a problem in pure Java code - the NativeThing instance in doStuff() is no longer required.

However, in our case, the NativeThing instance passed to the implementation of evaluateNativeThing() points to the same native structure as the instance created in doStuff(), so when the instance in doStuff() is finalized, the instance in evaluateNativeThing() will point to a non-existent structure and we'll get a SIGSEGV when we attempt to dereference the pointer.

We understand the need to optimize the code is ways such as this, and from a pure Java perspective the optimization is arguably a valid one. However, the optimization can clearly break code that is backed up by native structures via JNI.

Moreover, the optimization conflicts with the JLS, which states:

 "The scope of a local variable declaration in a block is the rest of
  the block in which the declaration appears, starting with its
  own initializer"


In the case we've highlighted here, the local variable "nt" clearly does NOT stay in scope for the whole of the block in which its declaration appears.

EVALUATION > I'm assuming an object cannot die *before* a local reference to it has gone out of scope. This can not be assumed. Neither the Java spec nor the JVM spec guarantees this. Just because a variable is in scope, doesn't mean the object it points to is reachable. Usually it is the case that an object pointed to by an in-scope variable is reachable, but yours is a case where it is not. The compiler can determine at jit time which variables are dead and does not include such variables in the oop-map. Since the object pointed to by "nt" can be reached from any live variable, it is eligible for collection. Rather than pass the "long" structure pointer to a native method via JNI, your program should pass the actual Java object to the native method. The native method can then fetch the structure pointer from Java object via jni. This parameter will acheive the effect you desire because JNI will create a handle for the parameter that keeps the object reachable until the native method returns.

EVALUATION Nowhere in either the JLS or the JVMS is there a suggestion that scope has anything to do with when a reference can be garbage collected. The JLS says something along the line of a "a reference may be collected when it no longer participates in a computation" which gives a lot of leeway to GC implementations. Scope is a notion that applies to resolution of names in source code. There's no representation of program scopes in the bytecodes that the JVM could even pay attention to. The JVM only worries about the lifetime of objects which ends after the last meaningful use of an object, where meaningful is an implementation dependent notion. However, your native methods will always constitute meaningful uses and you should program to take advantage of that. If you want the lifetime of a Java object to control the lifetime of a native piece of memory then you have to program very carefully. One primary rule is that you never pass around the long that represents your native pointer separately from the Java object. If you pass the long into your native method separately, then you must also pass the object that controls it's lifetime. I'm assuming evaluateNativeThing looks something like this: public void evaluateNativeThing(NativeThing nt) { myNativeMethod(nt.myLong); } It either needs to be myNativeMethod(nt) or myNativeMethod(nt, nt.myLong) if you want to guarantee their lifetimes are the same.

WORK AROUND Workarounds that keep an object alive may not be entirely reliable. For example, with the above suggestion, inlining may find that toString() is devoid of side effects, and escape analysis may show that "nt" is dead. Although it may not be capable today, a future JIT may eliminate the toString() code completely, re-exposing the current bug.

WORK AROUND 1. Exclude doStuff() from JIT compilation. 2. Add another line of code after the call to evaluateNativeThing(), to artificially keep the NativeThing instance in scope until evaluateNativeThing() returns. For example, modify the method to something like this: public void doStuff() { NativeThing nt = new NativeThing(); evaluateNativeThing(nt); nt.toString(); }