第五章 本地和全局引用
JNI将实例和数组类型(例如jobject、jclass、jstring和jarray)公开为不透明引用。本地代码不能直接检查不透明引用指针的内容。而是通过JNI函数来获取不透明引用所指向的数据结构。通过处理不透明引用,你不必担心依赖于特定Java虚拟机的内部对象数据结构布局。但是,在JNI中,你需要了解更多有关于不同类型的引用:
- JNI支持三中透明引用:本地引用,全局引用和弱全局引用
- 本地和全局引用拥有不同的生命周期。本地引用会被自动回收,而全局引用和弱全局引用会一直存在直到程序员将其释放
- 一个本地或全局引用保持被引用的对象不会被垃圾回收。但是一个弱全局引用允许被引用的对象呗垃圾回收。
- 并非所有的引用都可以在所有的上下文中使用。例如,在创建引用返回后的本地代码中使用本地引用是非法的。
在本章中,我们将详细讨论这些问题。 正确管理JNI引用对于编写可靠和节省空间的代码至关重要。
5.1 本地和全局引用
什么是本地和全局引用,以及他们有什么不同呢?我们会用一系列的实例来说明本地和全局引用。
5.1.1 本地引用
大多数JNI方法会创建本地引用。例如,JNI方法NewObject创建一个新的实例对象并返回引用该对象的本地引用。
本地引用仅在创建它的本地方法的动态上下文中有效,并且仅在该方法的一次调用中有效。在本地方法执行期间创建的所有本地引用将在本地方法返回后被释放。
不能在本地方法中通过静态变量来储存本地引用,并在后续调用中使用相同的引用。例如下面的代码,是4.4.4节的MyNewString方法的修改版本,在这里使用了不正确的本地引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* 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, “java/lang/String”); if (stringClass == NULL) { return NULL; } } /* It is wrong to use the cached stringClass here, because it may be invalid. */ cid = (*env)->GetMethodID(env, stringClass, "<init>", "([C)V"); ... elemArr = (*env)->NewCharArray(env, len); ... result = (*env)->NewObject(env, stringClass, cid, elemArr); (*env)->DeleteLocalRef(env, elemArr); return result; } |
这里已经消除了和我们将要讨论的没有直接关系的行。在静态变量中缓存stringClass的目的可能是想消除重复执行如下函数调用的开销:
1 |
FindClass(env, "java/lang/String"); |
这不是正确的方法,因为FindClass返回一个指向java.lang.String类对象的本地引用。下面分析为什么这样会引发问题,假设C.f本地方法实现调用MyNewString:
1 2 3 4 5 |
JNIEXPORT jstring JNICALL Java_C_f(JNIEnv *env, jobject this) { char *c_str = ...; ... return MyNewString(c_str); } |
在本地方法C.f调用返回后,虚拟机会释放所有在Java_C_f运行期间创建的本地引用。这些释放的本地引用包括对储存在stringClass变量中的类对象的本地引用。在后面,MyNewString调用将会尝试使用一个无效的本地引用,这可能导致内存损坏或者引发系统崩溃。例如,如下的代码片段使两个连续的调用到C.f并导致MyNewString遇到无效的本地引用:
1 2 3 4 |
... ... = C.f(); // The first call is perhaps OK. ... = C.f(); // This would use an invalid local reference. ... |
有两种方法能够使一个本地方法无效。如前所述,在本地方法返回后,虚拟机会自动的释放所有在该本地方法执行期间创建的本地引用。另外,程序员可能想使用JNI函数例如,DeleteLocalRef来显示的管理本地引用的生命周期。
如果虚拟机会在本地方法返回后自动释放,为什么还需要显示释放本地引用呢?本地引用防止被引用的对象被垃圾收集器回收,一直持续到本地引用无效为止。例如,在MyNewString中的DeleteLocalRef调用允许数组对象elemArr立即被垃圾收集器回收。否则,虚拟机只会在MyNewString调用的本地方法返回后(例如上面的C.f)释放elemArr对象。
一个本地引用在其销毁之前,可能传递给多个本地方法。例如,MyNewString方法返回一个通过NewObject创建的字符串引用。然后由MyNewString的调用者决定是否释放由MyNewString返回的本地引用。在Java_C_f例子中,C.f又作为本地方法调用的结果返回MyNewString的结果。在虚拟机从JAVA_C_f函数接收到本地引用后,他将底层字符串对象给c.f的调用者然后销毁最初由JNI函数NewObject创建的本地引用,然后销毁最初由JNI函数NewObject创建的本地引用。
地引用当然仅在创建它的线程中有效。在一个线程中创建的本地引用不能够在其他线程中使用。在本地方法中将本地引用储存在全局变量中,并期望在另一个线程中使用是一个编程错误。
5.1.2 全局引用
你可以在跨多个本地方法调用中使用全局变量。全局引用可以跨多线程使用并保持有效,直到程序员释放它为止。和本地引用一样,一个全局引用能够确保被引用的对象不会被垃圾收集器回收。
和本地引用不同的是,本地引用可以通过大多数JNI函数创建,但是全局引用只可以通过一个JNI方法(NewGlobalRef)创建。接下来的MyNewString版本显示如何使用全局引用。我们突出显示下面的代码和在上一节中错误地缓存本地引用的代码之间的区别:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/* This code is OK */ jstring MyNewString(JNIEnv *env, jchar *chars, jint len) { static jclass stringClass = NULL; ... if (stringClass == NULL) { jclass localRefCls = (*env)->FindClass(env, "java/lang/String"); if (localRefCls == NULL) { return NULL; /* exception thrown */ } <b> /* Create a global reference */ stringClass = (*env)->NewGlobalRef(env, localRefCls); /* The local reference is no longer useful */ (*env)->DeleteLocalRef(env, localRefCls); /* Is the global reference created successfully? */ if (stringClass == NULL) { return NULL; /* out of memory exception thrown */ } </b> } ... } |
这个修改版本中将从FindClass返回的本地引用传送给NewGlobalRef,该方法创建一个指向java.lang.String对象的全局引用。我们检查在删除localRefCls后NewGlobalRef是否成功创建了stringClass,因为这两种情况下都需要删除本地引用localRefCls。
5.1.3 弱全局引用
弱全局引用是在Java 2 JDK 1.2中新加入的。弱全局引用通过NewGlobalWeakRef创建,通过DeleteGlobalWeakRef释放。和全局引用一样,弱全局引用跨本地方法调用和跨线程调用依旧有效。而和全局引用不同的是,弱全局引用不能防止底层数据对象被垃圾收集器回收。
MyNewString示例显示了如何缓存java.lang.String的全局引用。MyNewString示例可以使用弱全局引用来缓存java.lang.String类。我们是使用全局引用还是弱全局引用并不重要,因为java.lang.String是一个系统类,永远不会被垃圾收集器回收。当本机代码缓存的引用不能使底层对象不被垃圾回收时,弱全局引用变得更加有用。例如,假设一个本地方法mypks.MyCls.f对类mypks.MyCls2进行缓存。在弱全局引用中缓存类仍然允许mypkg.MyCls2被卸载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
JNIEXPORT void JNICALL Java_mypkg_MyCls_f(JNIEnv *env, jobject self) { static jclass myCls2 = NULL; if (myCls2 == NULL) { jclass myCls2Local = (*env)->FindClass(env, "mypkg/MyCls2"); if (myCls2Local == NULL) { return; /* can’t find class */ } myCls2 = NewWeakGlobalRef(env, myCls2Local); if (myCls2 == NULL) { return; /* out of memory */ } } ... /* use myCls2 */ } |
我们假设MyCls和MyCls2有相同的生命周期(例如,它们可能是通过同一个类加载器加载的)。但是我们没有考虑到这样一个场景,在MyCls及其本地方法实现Java_mypks_MyCls仍在使用的情况下,MyCls2卸载并在稍后重新加载。如果这种情况发生了,我们必须检查缓存的弱引用是否仍然指向一个存活的类对象,或者指向已经没垃圾收集器回收的类对象。下一节将介绍如何对弱全局引用执行此类检查。
5.1.4 引用比较
在给定的两个本地、全局、弱全局引用中,可以使用IsSameObject方法来检查它们是否引用同一个对象。例如:
1 |
(*env)->IsSameObject(env, obj1, obj2) |
如果obj1和obj2引用同一个对象,那么这个方法就放回JNI_TRUE(或者1),否则返回JNI_FALSE(或者0)。
在JNI中,NULL引用是指向Java虚拟机中的空对象,如果obj是被本地或者全局引用,则可以使用:
1 |
(*env)->IsSameObject(env, obj, NULL) |
或者
1 |
obj == NULL |
来确认obj是否引用一个空对象。
弱引用对象使用的规则有一些不同。NULL弱引用引用空对象。 然而,IsSameObject对于弱全局引用具有特殊用途。您可以使用IsSameObject来确定非NULL弱全局引用是否仍然指向一个活动对象。假设wobj是一个非NULL的弱全局引用。以下调用:
1 |
(*env)->IsSameObject(env, wobj, NULL) |
如果wobj引用一个已经被回收的对象,则返回JNI_TRUE,如果wobj仍然引用一个存活的对象,则返回JNI_FALSE。
5.2 释放引用
除了被引用对象占用的内存外,每个JNI引用本身都会消耗一定量的内存。作为一个JNI程序员,你应该了解到你的程序在一个给定的时间内,将会使用的引用数量。特别是,你应该意识到你的程序在执行期间的某个时间点上,允许创建的本地引用数量的上限,即使这些本地引用最后会被虚拟机自动释放。暂时性的过多创建引用,可能会导致内存耗尽。
5.2.1 释放本地引用
在大多数情况下,在实现一个本地方法的时候,你不用过多的考虑释放本地对象。当本地方法返回到调用者处时,Java虚拟机会为你释放它们。但是JNI程序员有时候应该显示的释放本地引用以避免内存占用过多。考虑一下情况:
- 你需要在一个本地方法调用中创建大量的本地引用。这可能导致JNI内部本地引用表溢出,因此立即删除那些不再需要的本地引用将是一个好方法。例如,在以下程序段中,本地代码有遍历一个大的字符串数组的可能。每次迭代后,本地代码应该显示的释放对字符串元素的本地引用。如下所示:
1 2 3 4 5 |
for (i = 0; i < len; i++) { jstring jstr = (*env)->GetObjectArrayElement(env, arr, i); ... /* process jstr */ (*env)->DeleteLocalRef(env, jstr); } |
- 你想编写一个从未知上下文中调用的函数。在第4.3节中显示的MyNewString示例说明使用DeleteLocalRef在函数中快速删除本地引用,否则每次调用MyNewString函数后都会分配两个本地引用。
- 你的本地方法不会返回。一个本地方法可能进入无限事件调度循环中,释放在循环内创建的本地引用将是非常重要的,这样它们就不会无限积累,导致内存泄漏。
- 你的本地方法访问一个大对象,因而需要创建该对象的本地引用。然后,native方法在返回给调用者前可以进行额外的计算。对大对象的本地引用将阻止对象在本地方法返回前本垃圾收集器回收,即使对象不再在本地方法的剩余部分使用。例如在以下程序片段中,由于事先已经显示的调用DeleteLocalRef了,因此在执行函数longyComputation时,垃圾收集器可能会释放lref引用的对象。
1 2 3 4 5 6 7 8 |
/* A native method implementation */ JNIEXPORT void JNICALL Java_pkg_Cls_func(JNIEnv *env, jobject this) { lref = ... /* a large Java object */ ... /* last use of lref */ (*env)->DeleteLocalRef(env, lref); lengthyComputation(); /* may take some time */ return; /* all local refs are freed */ } |
5.2.2 在Java 2 JDK 1.2中管理本地引用
Java 2 JDK 1.2中提供了一组而外的函数用于管理本地应用的生命周期。这些函数是EnsureLocalCapacity、NewLocalRef、PushLocalFrame和PopLocalFrame。
JNI规范规定虚拟机能够自动确保每个本地方法能够创建至少16个本地引用。经验表明,除了与虚拟机中的对象进行复杂的交互外,对于大多数本地方法,这个数量已经足够。但是如果,需要创建而外的本地引用,那么本地方法可能会发出一个EnsureLocalCapacity调用,以确保有足够的本地引用空间。例如,上述示例的轻微变化为循环执行期间创建的所有本地参考提供足够的容量,如果有足够的内存可用:
1 2 3 4 5 6 7 8 9 |
/* The number of local references to be created is equal to the length of the array. */ if ((*env)->EnsureLocalCapacity(env, len)) < 0) { ... /* out of memory */ } for (i = 0; i < len; i++) { jstring jstr = (*env)->GetObjectArrayElement(env, arr, i); ... /* process jstr */ /* DeleteLocalRef is no longer necessary */ } |
当然和之前立即删除本地引用的版本相比,上面的版本需要消耗更多的内存。或者,Push / PopLocalFrame函数允许程序员创建本地引用的嵌套范围。 例如,我们也可以重写同样的例子,如下所示:
1 2 3 4 5 6 7 8 9 |
#define N_REFS ... /* the maximum number of local references used in each iteration */ for (i = 0; i < len; i++) { if ((*env)->PushLocalFrame(env, N_REFS) < 0) { ... /* out of memory */ } jstr = (*env)->GetObjectArrayElement(env, arr, i); ... /* process jstr */ (*env)->PopLocalFrame(env, NULL); } |
PushLocalFrame为特定数量的本地引用创建一个新的范围。PopLocalFrame破坏超出的范围,释放该范围内的所有本地引用。使用Push/PopLocalFrame的好处是它们可以管理本地引用的生命周期,而无需担心在执行过程中可能会创建的每个本地引用。在上面的例子中,如果处理jstr的计算创建了额外的本地引用,则这些本地引用将会在PopLocalFrame返回后被释放。
当你编写期望返回一个本地引用的实例程序时,NewLocalRef函数很有用。我们将在5.3节中演示NewLocalRef函数的用法。
本地代码可能会创建超出默认容量16或者PushLocalFrame或者EnsureLocalCapacity调用中保留的容量的本地引用。虚拟机将会尝试分配本地引用所需的内存。然而不能保证,这些内存是可用的。如果分配内存失败,虚拟机将会退出。你应该为本地引用保留足够的内存和尽快释放本地引用以避免这种意外的虚拟机退出。
Java 2 JDK 1.2提供了一个命令行参数-verbose:jni。当使能这个参数,虚拟机会报告超过预留容量的本地引用创建情况。
5.2.3 释放全局变量
当你的本地代码不再需要访问一个全局引用时,你应该调用DeleteGlobalRef方法。如果你忘记调用这个函数,虚拟机将无法通过垃圾收集器回收相应的对象,即使这个对象再也不会在系统的其他地方中使用。
当你的本地代码不再需要访问一个弱全局引用时,你应该调用DeleteWeakGlobalRef方法。如果你忘记调用这个函数,Java虚拟机仍然能够通过垃圾收集器收集底层对象,当时将无法回收该弱全局引用对象占用的内存。
5.3 引用管理规范
我们现在已经准备好基于我们前面的几节介绍的内容,来处理在本地代码中管理JNI引用的规则。目的是消除不必要的内存使用和对象保留。
通常来说有两种本地代码,直接实现在任意上下文中使用的本地方法和效用函数的函数。
当编写直接实现本地方法时,你需要注意避免在循环中过多的创建本地引用以及由不返回的本地方法创建的不需要的本地引用。在本地方法返回后,留下最多16个本地引用由虚拟机删除是可以接受的。本地方法调用不能导致全局引用或者弱全局引用累积,因为全局引用和弱全局引用在本地方法返回后不会释放。编写本机实用程序函数时,必须注意不要在整个函数中的任何执行路径上泄漏任何本地引用。因为效用函数可以从意料之外的上下文重复调用,任何不必要的引用创建都可能导致内存溢出。
- 当一个返回基本类型的函数被调用时,它不会产生额外的本地、全局、弱全局引用累积副作用。
- 当一个返回引用类型的函数被调用,它不能有本地、全局、弱全局引用的额外累积,除非这个引用被当做返回值。
为了缓存的目的,一个函数创建一些全局或弱全局引用是可以接受的,因为仅在第一次调用的时候会创建这些引用。
如果一个函数返回一个引用,你应该使用函数规范的返回引用部分。他不应该在某些时候返回本地引用,而在其他时候返回全局引用。调用者需要知道函数的返回类型,以便正确的管理自己的JNI引用。例如,以下代码重复的调用了一个函数GetInfoString。我们需要知道GetInfoString返回的引用类型,以便能够在每次迭代后正确释放返回的JNI引用。
1 2 3 4 5 |
while (JNI_TRUE) { jstring infoString = GetInfoString(info); ... /* process infoString */ ??? /* we need to call DeleteLocalRef, DeleteGlobalRef, or DeleteWeakGlobalRef depending on the type of reference returned by GetInfoString. */ } |
在Java 2 JDK 1.2中,NewLocalRef函数经常用于确保函数返回一个本地引用。为了说明,让我们对MyNewString函数进行另一个(有点设计的)更改。以下版本在全局引用中缓存经常请求的字符串(例如“CommonString”):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
jstring MyNewString(JNIEnv *env, jchar *chars, jint len) { static jstring result; /* wstrncmp compares two Unicode strings */ if (wstrncmp("CommonString", chars, len) == 0) { /* refers to the global ref caching "CommonString" */ static jstring cachedString = NULL; if (cachedString == NULL) { /* create cachedString for the first time */ jstring cachedStringLocal = ... ; /* cache the result in a global reference */ cachedString =(*env)->NewGlobalRef(env, cachedStringLocal); } return (*env)->NewLocalRef(env, cachedString); } ... /* create the string as a local reference and store in result as a local reference */ return result; } |
正常的代码路径返回作为本地引用的字符串。正如前面解释的那样,我们必须在一个全局引用中保存缓存的字符串,让其能够被多个本地方法和多个线程访问。加粗显示的行创建一个新的引用对象,其引用同一个缓存在全局引中的对象。作为其调用者契约的一部分,MyNewString经常返回一个本地引用。
Push/PopLocalFrame方法对于管理本地引用的声明周期是非常简便的。如果你在一个本地方法的入口调用PushLocalFrame,需要在本地方法返回前调用PopLocalFrame以确保所有在本地方法执行期间创建的本地引用都会被回收。Push/PopLocalFrame函数是非常有效率的。强烈建议你使用它们。
如果你在函数的入口调用了PushLocalFrame,记得在程序的所有退出路径上调用PopLocalFrame。例如,下面的程序有一个PushLocalFrame调用,但是却需要多个PopLocalFrame调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
jobject f(JNIEnv *env, ...) { jobject result; if ((*env)->PushLocalFrame(env, 10) < 0) { /* frame not pushed, no PopLocalFrame needed */ return NULL; } ... result = ...; if (...) { /* remember to pop local frame before return */ result = (*env)->PopLocalFrame(env, result); return result; } ... result = (*env)->PopLocalFrame(env, result); /* normal return */ return result; } |
错误的放置PopLocalFrame调用回引起不确定的行为,例如导致虚拟机崩溃。
上面的例子也表明为什么有时指定PopLocalFrame的第二个参数是有用的。result本地引用最初在由PushLocalFrame构造的新框架中创建的。PopLocalFrame将其作为第二个参数,result转换为前一贞中的新的本地引用,然后弹出最顶层的框架。