Sei sulla pagina 1di 10

J D C

T E C H

T I P S
TIPS, TECHNIQUES, AND SAMPLE CODE

WELCOME to the Java Developer Connection(sm) (JDC) Tech Tips,


August 1, 2000. This issue is about the Java(tm) 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(tm) 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:
* Caching objects in JNI
* Accessing arrays in JNI
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 at
http://java.sun.com/docs/books/tutorial/native1.1/index.html
These tips were developed using Java(tm) 2 SDK, Standard Edition,
v 1.3.
This issue of the JDC Tech Tips is written by Stuart Halloway,
a Java specialist at DevelopMentor (http://www.develop.com/java).
You can view this issue of the Tech Tips on the Web at
http://developer.java.sun.com/developer/TechTips/2000/tt0801.html
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CACHING OBJECTS IN JNI
One of the features of JNI is that it allows your native code,
such as C++, to use Java objects. However this sometimes presents
a problem in dealing with the "lifetime" of objects, that is, the
time between an object's allocation and deallocation. Java manages
an object's allocation through new, and indirectly manages its
deallocation through garbage collection. However C++ requires
explicit control of the entire lifetime through new and
delete. Because JNI straddles both the world of the Java language
and C++, an awkward compromise must be reached. JNI provides
explicit mechanisms to manage an object's lifetime, as in C++. But
these mechanisms do not directly control lifetime. Instead, they
give hints to the Java garbage collector. This creates a difficult
situation for the developer. JNI object references have
non-deterministic destruction, as in the Java environment; you
cannot determine specifically when an object's resources will be
reclaimed. And misusing JNI object references can crash the entire
process, as in C++!
This tip shows you how to correctly manage an object's
deallocation in your JNI code.
Let's look at a simple example that uses native code to find the
maximum value in an array of integers:

//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 call 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 programminglanguage 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->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("nativeMaxCritical not implemented yet");
if (!msg) return 0;
env->CallVoidMethod(objOut, midPrintln, msg);
return 0;
}
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(tm) Runtime Environment (JRE) and
underlying OS you are using, one of several things might happen:
- the program might crash
- the program might run normally
- the program might fail with a "FATAL ERROR in native method"
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.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ACCESSING ARRAYS IN JNI
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
The second block needs to be replaced with the following:
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;
}
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
http://staff.develop.com/halloway/JavaTools.html.
--------------------------------------------------------------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(tm) 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:
(1) Copying arrays is expensive. In the one case (1.3 nativeMax)
where the array was copied, performance was noticeably slower.
(2) Native code is not always faster then equivalent Java code.
Even when native code is faster, it doesn't represent an
order of magnitude improvement.
(3) It is difficult to benchmark HotSpot code. HotSpot tends to
fare poorly on benchmarks, but to shine in real applications
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:
o The Java Native Interface: Programmer's Guide and Specification
(Java Series), by Sheng Liang
(http://java.sun.com/docs/books/jni/index.html).
o Java Platform Performance Strategies and Tactics (Java Series),
by Steve Wilson and Jeff Kesselman. There is a very
interesting chapter on JNI performance.
(http://java.sun.com/docs/books/performance/).
. . . . . . . . . . . . . . . . . . . . . . .
- NOTE
The names on the JDC mailing list are used for internal Sun
Microsystems(tm) 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@sun.com
- SUBSCRIBE/UNSUBSCRIBE
The JDC Tech Tips are sent to you because you elected to subscribe
when you registered as a JDC member. To unsubscribe from JDC email,
go to the following address and enter the email address you wish to
remove from the mailing list:
http://developer.java.sun.com/unsubscribe.html
To become a JDC member and subscribe to this newsletter go to:
http://java.sun.com/jdc/
- ARCHIVES
You'll find the JDC Tech Tips archives at:
http://developer.java.sun.com/developer/TechTips/index.html
- 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:
http://developer.java.sun.com/developer/copyright.html
JDC Tech Tips
August 1, 2000

Potrebbero piacerti anche