|
August 01, 2000 WELCOME to the Java Developer Connection (JDC) Tech Tips, August 01, 2000. This issue is about the JavaTM Native Interface (JNI). JNI is a powerful tool for building Java applications that interoperate with other languages, especially C++. An important thing to understand when you use JNI to integrate C++ code into a program written in the Java programming language is how JNI forces the Java and C++ memory management models to coexist in one process.This issue of the JDC Tech Tips covers two memory management issues that arise in JNI programming: This issue of the JDC Tech Tips is written by Stuart Halloway, a Java specialist at DevelopMentor. These tips were developed using Java 2 SDK, Standard Edition, v 1.3. These tips assume that you have some familiarity with JNI and that you know how to compile native JNI libraries with your C++ compiler of choice. If you are unfamiliar with JNI, see the Java Native Interface trail in the Java tutorial.
| |||||||||||||||||||||||||||||||||||
//java code Max.java
import java.util.*;
public class Max {
public static final int ARRAY_SIZE = 1000;
public static int[] arr = initAnArray();
static {
System.loadLibrary("Max");
}
public static int[] initAnArray() {
int[] arr = new int[ARRAY_SIZE];
Random rnd = new Random();
for (int n=0; n<ARRAY_SIZE; n++) {
arr[n]= rnd.nextInt();
}
return arr;
}
public static int max(int[] nums) {
int length = nums.length;
int max = Integer.MIN_VALUE;
int current = 0;
for (int n=0; n<length; n++) {
current = nums[n];
if (current > max) {
max = current;
}
}
return max;
}
public static native int
nativeMax(int[] mins);
public static native int
nativeMaxCritical(int[] mins);
public static void main(String [] args) {
System.out.println("max=" + max(arr));
//System.out.println("nativeMax=" +
nativeMax(arr));
//System.out.println("nativeMaxCritical=
" + nativeMaxCritical(arr));
}
}
|
This program calls a max function that is implemented in Java code.
There are also calls, initially commented out, to two other
versions of the max function: nativeMax() and
nativeMaxCritical().
When the calls are uncommented, the functions will need native
language implementations, such as C++.
It would be nice if the native code could take advantage of
certain Java programming language features, such as using
System.out.println() for logging messages to the console.
One way to add this feature is to implement the JNI_OnLoad
method in your C++ library:
//C++ CODE Max.cpp
#include <jni.h>
#include <limits.h>
//cache the methodID and object
//needed to call System.out.println
static jmethodID midPrintln;
static jobject objOut;
extern "C" {
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM
*vm, void *reserved)
{
JNIEnv* env = 0;
jclass clsSystem = 0;
jclass clsPrintStream = 0;
jfieldID fidOut = 0;
jstring msg = 0;
if (JNI_OK != vm->GetEnv((void **)&env,
JNI_VERSION_1_2)) {
return JNI_ERR;
}
clsSystem = env->FindClass("
java/lang/System");
if (!clsSystem) return JNI_ERR;
clsPrintStream = env->FindClass("
java/io/PrintStream");
if (!clsPrintStream) return JNI_ERR;
fidOut = env-&gr;GetStaticFieldID(
clsSystem, "out",
"Ljava/io/PrintStream;");
if (!fidOut) return JNI_ERR;
objOut = env->GetStaticObjectField(
clsSystem, fidOut);
if (!objOut) return JNI_ERR;
midPrintln = env->GetMethodID(
clsPrintStream, "println", "
(Ljava/lang/String;)V");
if (!midPrintln) return JNI_ERR;
msg = env->NewStringUTF("MAX
library loaded");
if (!msg) return JNI_ERR;
env->CallVoidMethod(objOut, midPrintln,
msg);
return JNI_VERSION_1_2;
}
}
|
The JNI_OnLoad entry point is called once, when the native library
is loaded by a call to System.loadLibrary. In this example, the
line:
env->CallVoidMethod(objOut,
midPrintln, msg);
actually does the work of calling System.out.println. Before this
line of code can execute, some preparation must take place. The
calls to FindClass return jclass references to
java.lang.System
(to reach the out field) and java.io.PrintStream (to reach the
println method). In JNI fields and methods must be accessed by
first requesting an ID, done here by the GetStaticFieldID and
GetMethodID methods. Finally, the string to be printed must be
allocated using the NewStringUTF helper method. Notice that
midPrintln and objOut are cached in static variables.
This helps
avoid having to do all the preparation work the next time
System.out.println is used. Cacheing is also an important
performance optimization in JNI--you do not want to repeatedly
look up objects and ids.
Compile both the Java code and the C++ code into the same directory. Then run the program from that directory using the command:
java -cp . Max
You should see the output "MAX library loaded." in your
System.out.
Although this code seems to work, it does not correctly manage object references. Referring back to the code, notice that the methods on the JNIEnv* fall into two categories: (1) those that return IDs, and (2) those that return some type of object reference. You do not need to worry about the IDs because they do not represent any special claim on resources. The methods and fields are there as long as the class is loaded, whether you use them from JNI or not. The object references are more challenging. Unless otherwise documented, all JNI methods return local references. A local reference is a thread-local, method-local handle to a Java object. In other words, you have permission to use the object only for the duration of the JNI method, and only from the calling thread. This gives the garbage collector a well-defined opportunity to collect the object, that is, when you return from a method.
The JNI_OnLoad method above obtains four local references:
clsSystem, clsPrintStream, objOut, and msg. Each of
these
references is valid only for the duration of the JNI_OnLoad call.
For clsSystem, clsPrintStream, and msg, this is
exactly what you
want; these objects are only used within the method. Just as in
the Java programming language, you do not have to worry about
deallocating these objects. Garbage collection will take care
of them. However the objOut handle is processed differently. It
is cached in a static variable for later use. This leads to
undefined behavior, that is, there is no guarantee that the
handle is still valid. The following native methods demonstrate
the problem:
//make sure these are inside
//the extern "C" block
JNIEXPORT jint JNICALL Java_Max_nativeMax
(JNIEnv *env, jclass, jintArray arr)
{
jstring msg = env->NewStringUTF(
"nativeMax not implemented yet");
if (!msg) return 0;
env->CallVoidMethod(objOut, midPrintln,
msg);
return 0;
}
JNIEXPORT jint JNICALL Java_Max_nativeMaxCritical
(JNIEnv *env, jclass, jintArray arr)
{
jstring msg = env->
NewStringUTF("in nativeMaxCritical");
if (!msg) return 0;
env->CallVoidMethod(objOut, midPrintln, msg);
long length = env->GetArrayLength(arr);
long max = INT_MIN;
long current = 0;
jboolean isCopy = JNI_FALSE;
long* elems = (long*) env->
GetPrimitiveArrayCritical(arr, &isCopy);
if (!elems) return 0; //exception already pending
for (int n=0; n<length; n++) {
current = elems[n];
if (current > max) {
max = current;
}
}
env->ReleasePrimitiveArrayCritical(arr, elems, JNI_ABORT);
return max;
}
|
In the next tip, these methods will have complete implementations,
but for now they just use System.out.println to report that they
are incomplete. Go back and uncomment the calls to nativeMax and
nativeMaxCritical in Max.main, and try running the
program.
Depending on which Java Runtime Environment
(JRE) and
underlying OS you are using, one of several things might happen:
This kind of unpredictable behavior never happens in Java programs, but is standard for C++ programs. Unfortunately, JNI code is similar to C++ code in that the behavior of the code that mismanages memory is undefined. Undefined behavior is much worse than a simple crash because you might not realize there is a program bug. This is particularly true if the code often runs normally (sometimes known as the "it worked on my machine" syndrome). Undefined behavior makes finding code defects very difficult.
JRE 1.2 and the classic VM of JRE 1.3 have a non-standard
command line option that can help you track down JNI bugs. Try
running the program again with the -Xcheck:jni option. If you
are running JRE 1.3, you will have to select the classic VM
with the classic option:
(if 1.2) java -cp . -Xcheck:jni Max (if 1.3) java -classic -cp . -Xcheck:jni Max
If you are lucky, you will get the following descriptive error:
FATAL ERROR in native method: Bad global
or local ref passed to JNI
at Max.nativeMax(Native Method)
at Max.main(Max.java:75)
It is a good idea to use the -Xcheck:jni flag during
development, but you should not count on this to find all
JNI-related problems. The best approach is careful analysis
of your java object references, plus code review.
In the example above, fixing the objOut reference is a simple
matter. Instead of a local reference, objOut should be stored in
a global reference. While a local reference is bound to a thread
and method call, a global reference lives until you specifically
delete it. The NewGlobalRef function creates a global reference
to any existing reference. Modify the JNI_OnLoad function,
that is, replace the following lines in JNI_Onload:
objOut = env->GetStaticObjectField(
clsSystem, fidOut);
if (!objOut) return JNI_ERR;
with the following lines:
jobject localObjOut =
env->GetStaticObjectField(
clsSystem, fidOut);
if (!localObjOut) return JNI_ERR;
objOut = env->NewGlobalRef(localObjOut);
|
Notice that the static type of a global reference is the same as
the static type of a local reference (both are jobject). This
means that you must remember which references are global and which
are local; the compiler will not assist you. In the code above,
objOut holds a global reference which will prevent the garbage
collector from invalidating the reference. In this example,
a global reference provides exactly the desired behavior, keeping
the reference cached for the lifetime of the application. If you
need a reference to live longer than a method, but not forever,
you can match the call to NewGlobalRef() with a subsequent call to
DeleteGlobalRef().
If you recompile the C++ library with this new code, Max should
run correctly, and -Xcheck:jni should not report any problems.
Now that the object references are in order, it is time to actually implement the max method in C++ code. If you scanned jni.h, you would find three methods that offer access to an array of Java integers:
GetIntArrayRegion(jintArray, jsize start,
jsize len, jint *buf)
jint* GetIntArrayElements(jintArray array,
jboolean *isCopy)
void* GetPrimitiveArrayCritical(jintArray array,
jboolean *isCopy)
|
While each of these methods can be used to access any int array, they have radically different semantics and performance characteristics. Choosing the right one is critical to writing correct, high-performance code.
GetIntArrayRegion is the simplest to use, because you never touch
the actual array data. Instead, you allocate a buffer, and some
portion of the array is copied into your buffer. Because the array
is copied, GetIntArrayRegion is rarely the best option for high
performance.
GetIntArrayElements asks the JRE to give you a pointer into
the actual array data. Sharing array memory with the JRE is
is called "pinning" the array, and when you are done you
must unpin the array with a call to ReleaseIntArrayElements.
Think of GetIntArrayElements as a polite request for a pointer
to the array data; it is not a demand for a pointer. You can use
the isCopy parameter to find out if your data is the actual array
data or your own private copy.
GetPrimitiveArrayCritical was added to JDK 1.2 to improve the
performance of array operations. Like GetIntArrayElements, the
critical API also asks the JRE for a pointer to the real data,
but this time the question is more of a demand. The critical API
tells the JRE to do everything possible to provide direct access.
This can include blocking other threads and even disabling all
garbage collection to guarantee safe access to the array data.
Because the JRE might be blocking many other operations while
you are accessing the array, you should exit the critical region
as soon as possible. Do this by calling
ReleasePrimitiveArrayCritical. Also, be careful not to call
other JNI functions, or do anything that could cause the current
thread to block.
Which array API is best for the max example? In the example, the
array data is traversed a single time and in read-only fashion.
This is a case where direct access to the data should provide a
substantial speedup. So you should probably use
GetIntArrayElements or GetPrimitiveArrayCritical.
Here's the code for each:
JNIEXPORT jint JNICALL Java_Max_nativeMax
(JNIEnv *env, jclass, jintArray arr)
{
jstring msg = env->NewStringUTF(
"in nativeMax");
if (!msg) return 0;
env->CallVoidMethod(objOut, midPrintln, msg);
jboolean isCopy = JNI_FALSE;
long* elems = env->GetIntArrayElements(
arr, &isCopy);
if (!elems) return 0;
//exception already pending
long length = env->GetArrayLength(arr);
long max = INT_MIN;
long current = 0;
for (int n=0; n<length; n++) {
current = elems[n];
if (current > max) {
max = current;
}
}
env->ReleaseIntArrayElements(arr,
elems, JNI_ABORT);
return max;
}
JNIEXPORT jint JNICALL
Java_Max_nativeMaxCritical
(JNIEnv *env, jclass, jintArray arr)
{
jstring msg = env->NewStringUTF("
in nativeMaxCritical");
if (!msg) return 0;
env->CallVoidMethod(objOut,
midPrintln, msg);
jboolean isCopy = JNI_FALSE;
long* elems = (long*)
env->GetPrimitiveArrayCritical(
arr, &isCopy);
if (!elems) return 0;
//exception already pending
long length = env->GetArrayLength(arr);
long max = INT_MIN;
long current = 0;
for (int n=0; n<length; n++) {
current = elems[n];
if (current > max) {
max = current;
}
}
env->ReleasePrimitiveArrayCritical(arr,
elems, JNI_ABORT);
return max;
}
|
Notice that the two versions of the code are almost identical.
They differ in the names of the Get/Release pair. The array code
itself is trivial. In fact, the only interesting detail is the
third parameter to the release function: JNI_ABORT. The
JNI_ABORT
flag specifies that if you are using a local copy of the array,
there is no need to copy back to the real array. If you wind up
working with a copy of the array, this is a major performance
savings. Since the array was never written to, it's silly to copy
it back.
The behavior of GetIntArrayElements and
GetPrimitiveArrayCritical
is not guaranteed. Either API can at any time return a copy or
a direct pointer to the data. This means that you have to test
your code on your specific JRE to determine whether you are
getting a performance boost from direct access.
Here is a summary of results obtained from testing the max
example on the 1.2 and 1.3 JREs. A debugger was used to check
the isCopy value. Benchmark code was used to compare the
performance of the three max implementations. You can find the
benchmark code at Stu Halloway's Java
Tools.
| Test | Copied Array? | Time (microsec) |
| 1.2 max | no | 18 |
| 1.2 nativeMax | no | 18 |
| 1.2 nativeMaxCritical | no | 15 |
| 1.3 max | no | 25 |
| 1.3 nativeMax | yes | 27 |
| 1.3 nativeMaxCritical | no | 15 |
|
Key: 1.2 tests are with classic VM, JIT 1.3 tests are with the Java HotSpot Server VM | ||
It would be unwise to jump to any conclusions from these results. The result will differ on different machines or with different sized arrays. However the results do suggest that:
Also, a simple looping benchmark cannot tell you much about the behavior of a heavily threaded (read: server) application. If a JRE blocks other threads in order to give direct access to memory, overall throughput can actually be worse with direct access to arrays. In that situation, it would be better to use the GetIntArrayRegion API to create a working copy of the array.
As you can see, JNI code becomes tricky to write as soon as you begin to do any serious work. You must explicitly manage the lifetime of objects by correctly choosing local or global references, and run tests to determine the array accessor that gives the best performance for your application.
For further information about JNI, see the following publications:
Note
The names on the JDC mailing list are used for internal Sun MicrosystemsTM purposes only. To remove your name from the list, see Subscribe/Unsubscribe below.
Feedback
Comments? Send your feedback on the JDC Tech Tips to: jdc-webmaster
Subscribe/Unsubscribe
To subscribe to these and other SDN publications:
- Go to the Sun Developer Network - Subscriptions page,
choose the newsletters you want to subscribe to and click
"Submit".
To unsubscribe,
- Go to the Sun Developer Network -
Subscriptions page,
uncheck the appropriate checkbox, and click "Submit".
Copyright
Copyright 2000 Sun Microsystems, Inc. All rights reserved.
901 San Antonio Road, Palo Alto, California 94303 USA.
This Document is protected by copyright. For more information, see:
|
| ||||||||||||