The Garbage Collector is too aggressive!!!
(back to blogging after a short hiatus...)
A customer recently complained to me that the GC is too aggressive, it can reclaim objects too early (Wow!), and the JIT compiler is broken for allowing the GC to do so (double Wow!)!
Here's the (simplified) example that the customer gave me:
class Foo {
// cPtr is really a pointer to a C object in the C heap
long cPtr;
long getCPtr() { return cPtr; }
void finalize() {
freeCObject(cPtr);
}
native void freeCObject(long cPtr);
}
class Bar {
native void processCObject(long cPtr);
void doSomething() {
Foo foo;
...
// foo now points to object F
...
long cPtr = foo.getCPtr();
processCObject(cPtr);
}
}
(If you're really into bug hunting please refrain from reading further
and try to find the race in the code...)
OK, here's what happens: the JIT can prove that variable foo is not accessed after the assignment to variable cPtr and, therefore, the GC will not consider foo as part of the stack frame's live root set for safepoints that take place after the assignment (basically, foo is considered "dead" after the assignment). Imagine now that a GC happens while the native method processCObject() is running (note: JNI will do a safepoint check upon entering a method and will suspend the thread if a safepoint has been requested). Also imagine that foo is the last reference to object F. Given that foo will not be considered as part of the stack frame's live root set (remember, it is considered "dead"), the GC can decide that F is actually unreachable and queue it up for finalization. At the end of the GC, the application threads, as well as the finalization thread, will be restarted and will race for accessing the C object; the application thread will try to process it, while the finalization thread will try to free it.
Ouch.
First, how can this happen? Well, as far as the JIT is concerned, cPtr is just another long and is treated as such. So, the JIT has no way of knowing that cPtr is actually a C pointer and the object it points to is associated with foo. Hence, it will not consider foo to be live as long as cPtr is live (the JIT just does not do that for longs!).
So, what can we do to fix this bug? Well, we just have to ensure that the GC considers foo live during the duration of the processCObject() method. There are a couple of ways of doing this:
- passing foo as an additional parameter to processCObject(), or
- making processCObject() a non-static method on the Foo class
Another recommendation that makes it easier to avoid such problems, courtesy of Ross Knippel from our compiler group, is to avoid manipulating C-pointer-containing long fields from Java, but only manipulate them from within native methods. This way, there will always be a JNI handle pointing to the Java object that contains the C pointer which will ensure that the object will not be collected while the corresponding C object is accessed. In the above example, you will only pass foo to the processCObject() native method, which will first retrieve cPtr and then go ahead and process the corresponding C object.
This was a fun little problem (maybe, not this much fun for the customer who hit it!). What I'd like you to at least get out of this is that using native methods in Java is really not very straightforward and little problems like this one can cause major headaches. So, please, be careful!
Posted at 01:04PM Nov 28, 2006 by tony in Sun | Comments[1]
This is a common design pattern suggested in Sheng Liang's JNI book (called peer classes or proxy classes). I recently fixed this problem for SWIG (http://www.swig.org) and looked at the various solutions before choosing the suggestion of adding in an additional parameter.
I found that the solution of passing the Java object as an additional parameter (and not actually using it within the JNI code) gave a small performance hit of about 4-20% for 1-10 parameters respectively on Windows jdk-1.4.2. Another solution is to just pass the Java object instead of a long and using JNI methods to extract the C++ pointer from the long. This solution imposed a performance hit of 5100%-10500%. Choosing a solution with these kind of differences was easy :) I did bring the time down though for this latter solution by simple cacheing of the results from the FindClass and GetFieldID JNI method calls, but then there there are thread safety issues and it was still a lot slower than just using the additional parameter approach.
This is roughly what I do now:
jlong jarg1, jobject jarg1_ // the JNI method parameters Test *arg1 = *(Test **)&jarg1;which is just a simple C++ cast and is super fast, whereas for the JNI solution the code is:
jobject jarg1 // the JNI method parameter Test *arg1; /*static*/ jclass clazz = jenv->FindClass("Test"); /*static*/ jfieldID fid = jenv->GetFieldID(clazz, "swigCPtr", "J"); jlong cPtr = jenv->GetLongField(jarg1, fid); Test *arg1 = *(Test **)&cPtr;The cacheing solution simply uncomments the static keywords commented out above. It is the three JNI calls FindClass, GetFieldID, GetLongField which take so long.
If only language Interoperability with C/C++ was easier!
Posted by William Fulton on January 19, 2007 at 12:18 PM EST #