Received: from PACIFIC-CARRIER-ANNEX.MIT.EDU by po10 (5.61/4.7) id AA01995; Tue, 1 Aug 00 17:12:31 EDT Received: from hermes.javasoft.com by MIT.EDU with SMTP id AA10454; Tue, 1 Aug 00 17:07:03 EDT Received: (from nobody@localhost) by hermes.java.sun.com (8.9.3+Sun/8.9.1) id VAA24965; Tue, 1 Aug 2000 21:08:36 GMT Date: Tue, 1 Aug 2000 21:08:36 GMT Message-Id: <200008012108.VAA24965@hermes.java.sun.com> X-Authentication-Warning: hermes.java.sun.com: Processed from queue /bulkmail/data/ed_6/mqueue5 X-Mailing: 234 From: JDCTechTips@sun.com Subject: JDC Tech Tips August 1, 2000 To: JDCMember@sun.com Reply-To: JDCTechTips@sun.com Errors-To: bounced_mail@hermes.java.sun.com Precedence: junk Mime-Version: 1.0 Content-Type: text/plain; charset=us-ascii X-Mailer: Beyond Email 2.2 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 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 #include //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 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 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. (http://java.sun.com/docs/books/performance/). There is a very interesting chapter on JNI 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