构造函数的方式是很有用的。尽管如此,大部分情况下,你应该使用NewObject,尽量避免使用容易出错的AllocObject/CallNonvirtualVoidMethod方法。
4.4 缓存字段ID和方法ID
获取字段ID和方法ID时,需要用字段、方法的名字和描述符进行一个检索。检索过程相对比较费时,因此本节讨论用缓存技术来减少这个过程带来的消耗。缓存字段ID和方法ID的方法主要有两种。两种区别主要在于缓存发生的时刻,是在字段ID和方法ID被使用的时候,还是定义字段和方法的类静态初始化的时候。
4.4.1 使用时缓存
字段ID和方法ID可以在字段的值被访问或者方法被回调的时候缓存起来。下面的代码中把字段ID存储在静态变量当中,这样当本地方法被重复调用时,不必重新搜索字段ID:
JNIEXPORT void JNICALL
Java_InstanceFieldAccess_accessField(JNIEnv *env, jobject obj) {
static jfieldID fid_s = NULL; /* cached field ID for s */
jclass cls = (*env)->GetObjectClass(env, obj); jstring jstr; const char *str;
if (fid_s == NULL) {
fid_s = (*env)->GetFieldID(env, cls, \
\ if (fid_s == NULL) {
return; /* exception already thrown */ } }
printf(\
jstr = (*env)->GetObjectField(env, obj, fid_s); str = (*env)->GetStringUTFChars(env, jstr, NULL); if (str == NULL) {
return; /* out of memory */ }
printf(\
(*env)->ReleaseStringUTFChars(env, jstr, str);
jstr = (*env)->NewStringUTF(env, \ if (jstr == NULL) {
return; /* out of memory */ }
(*env)->SetObjectField(env, obj, fid_s, jstr); }
由于多个线程可能同时访问这个本地方法,上面方法中的代码很可能会导致混乱,其实没事,多个线程计算的ID其实是相同的。
同样的思想,我们也可以缓存java.lang.String的构造方法的ID: jstring
MyNewString(JNIEnv *env, jchar *chars, jint len) {
jclass stringClass; jcharArray elemArr;
static jmethodID cid = NULL; jstring result;
stringClass = (*env)->FindClass(env, \ if (stringClass == NULL) {
return NULL; /* exception thrown */ }
/* Note that cid is a static variable */ if (cid == NULL) {
/* Get the method ID for the String constructor */ cid = (*env)->GetMethodID(env, stringClass, \ if (cid == NULL) {
return NULL; /* exception thrown */ } }
/* Create a char[] that holds the string characters */ elemArr = (*env)->NewCharArray(env, len); if (elemArr == NULL) {
return NULL; /* exception thrown */ }
(*env)->SetCharArrayRegion(env, elemArr, 0, len, chars);
/* Construct a java.lang.String object */
result = (*env)->NewObject(env, stringClass, cid, elemArr);
/* Free local references */
(*env)->DeleteLocalRef(env, elemArr); (*env)->DeleteLocalRef(env, stringClass); return result;
}
当MyNewString方法第一次被调用时,我们计算java.lang.String的构造方法的ID,并存储在静态变量cid中。
4.4.2 类的静态初始化过程中缓存字段和方法ID
我们在使用时缓存字段和方法的ID的话,每次本地方法被调用时都要检查ID是否已经被缓存。许多情况下,在字段ID和方法ID被使用前就初始化是很方便的。VM在调用一个类的方法和字段之前,都会执行类的静态初始化过程,所以在静态初始化该类的过程中计算并缓存字段ID和方法ID是个不错的选择。 例如,为了缓存InstanceMethodCall.callback的方法ID,我们引入了一个新的本地方法initIDs,这个方法在InstanceMethodCall的静态初始化过程中被调用。代码如下:
class InstanceMethodCall {
private static native void initIDs(); private native void nativeMethod(); private void callback() {
System.out.println(\ }
public static void main(String args[]) {
InstanceMethodCall c = new InstanceMethodCall(); c.nativeMethod(); }
static {
System.loadLibrary(\ initIDs(); } }
与4.2节中的代码相比,上面这段代码多了两行,initIDs方法简单地计算并缓存方法ID:
jmethodID MID_InstanceMethodCall_callback;
JNIEXPORT void JNICALL
Java_InstanceMethodCall_initIDs(JNIEnv *env, jclass cls) {
MID_InstanceMethodCall_callback =
(*env)->GetMethodID(env, cls, \ }
VM进行静态初始化时在调用任何方法前调用initIDs,这样方法ID就被缓存了全局变量中,本地方法的实现就不必再进行ID计算: JNIEXPORT void JNICALL
Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj) {
printf(\
(*env)->CallVoidMethod(env, obj,
MID_InstanceMethodCall_callback); }
4.4.3 两种缓存ID的方式之间的对比
如果JNI程序员不能控制方法和字段所在的类的源码的话,在使用时缓存是个合理的方案。例如在MyNewString当中,我们不能在String类中插入一个initIDs方法。
比起静态初始时缓存来说,使用时缓存有一些缺点: 1、 使用时缓存的话,每次使用时都要检查一下。 2、 方法ID和字段ID在类被unload时就会失效,如果你在使用时缓存ID,你必须确保只要本地代码依赖于这个ID的值,那么这个类不被会unload(下一章演示了如何通过使用JNI函数创建一个类引用来防止类被unload)。另一方面,如果缓存发生在静态初始化时,当类被unload和reload时,ID会被重新计算。
因此,尽可能在静态初始化时缓存字段ID和方法ID。
4.5 JNI操作JAVA中的字段和方法时的效率
学完了如何缓存ID来提高效率后,你可能会对使用JNI访问java字段和方法的效率不太明白,native/java比起java/native和java/java来的话,效率如何呢?
当然,这取决于VM的实现。我们不能给出在大范围的VM上通用的数据,但我们可以通过分析本地方法回调java方法和JNI操作字段以及方法的过程来给出一个大致的概念。
我们从比较java/native和java/java的效率开始。java/native调用比java/java要慢,主要有以下几个原因: 1、 java/native比起JVM内部的java/java来说有一个调用转换过程,在把控制权和入口切换给本地方法之前,VM必须做一些额外的操作来创建参数和栈帧。 2、 对VM来说,对方法调用进行内联比较容易,而内联java/native方法要难得多。
据我们的估计,VM进行java/native调用时的消耗是java/java的2~3倍。当然VM可以进行一些调整,使用java/native的消耗接近或者等于java/java的消耗。
技术上来讲,native/java调用和java/native是相似的。但实际上native/java调用很少见,VM通常不会优化native/java这种回调方式。多数VM中,native/java调用的消耗可以达到java/java调用的10倍。
使用JNI访问字段的花费取决于通过JNIEnv进行调用的消耗。以废弃一个对象引用来说,本地代码必须依赖于特定的JNI函数才能做到,而这个依赖是必须的,它把本地代码和VM中对象的内部形式很好地隔离开。
第五章 全局引用和本地引用
JNI提供了一些实例和数组类型(jobject、jclass、jstring、jarray等)作为不透明的引用供本地代码使用。本地代码永远不会直接操作引用指向的VM内部的数据内容。要进行这些操作,必须通过使用JNI操作一个不引用来间接操作数据内容。因为只操作引用,你不必担心特定JVM中对象的存储方式等信息。这样的话,你有必要了解一下JNI中的几种不同的引用: 1、 JNI支持三种引用:局部引用、全局引用、弱全局引用(下文简称“弱引用”)。 2、 局部引用和全局引用有不同的生命周期。当本地方法返回时,局部引用会被自动释放。而全局引用和弱引用必须手动释放。 3、 局部引用或者全局引用会阻止GC回收它们所引用的对象,而弱引用则不会。 4、 不是所有的引用可以被用在所有的场合。例如,一个本地方法创建一个局部引用并返回后,再对这个局部引用进行访问是非法的。 本章中,我们会详细地讨论这些问题。合理地管理JNI引用是写出高质量的代码的基础。
5.1 局部引用和全局引用
什么是全局引用和局部引用?它们有什么不同?我们下面使用一些例子来说明。
5.1.1 局部引用
大多数JNI函数会创建局部引用。例如,NewObject创建一个新的对象实例并返回一个对这个对象的局部引用。
局部引用只有在创建它的本地方法返回前有效。本地方法返回后,局部引用会被自动释放。
你不能在本地方法中把局部引用存储在静态变量中缓存起来供下一次调用时使用。下面的例子是MyNewString函数的一个修改版本,这里面使用局部引用的方法是错误的:
/* This code is illegal */ jstring
MyNewString(JNIEnv *env, jchar *chars, jint len) {
static jclass stringClass = NULL; jmethodID cid;
jcharArray elemArr; jstring result;
if (stringClass == NULL) {
stringClass = (*env)->FindClass(env,
\ if (stringClass == NULL) {
return NULL; /* exception thrown */