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,
"java/lang/String");
if (stringClass == NULL) {
return NULL; /* exception thrown */
}
}
/* 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;
}
上面代码中,我们省略了和我们的讨论无关的代码。因为FindClass返回一个对java.lang.String对象的局部引用,上面的代码中缓存stringClassr做法是错误的。假设一个本地方法C.f调用了MyNewString:
JNIEXPORT jstring JNICALL
Java_C_f(JNIEnv *env, jobject this)
{
char c_str = ...;
...
return MyNewString(c_str);
}
C.f方法返回后,VM释放了在这个方法执行期间创建的所有局部引用,也包含对String类的引用stringClass。当再次调用MyNewString时,会试图访问一个无效的局部引用,从而导致非法的内存访问甚至系统崩溃。
释放一个局部引用有两种方式,一个是本地方法执行完毕后VM自动释放,另外一个是程序员通过DeleteLocalRef手动释放。
既然VM会自动释放局部引用,为什么还需要手动释放呢?因为局部引用会阻止它所引用的对象被GC回收。
局部引用只在创建它们的线程中有效,跨线程使用是被禁止的。不要在一个线程中创建局部引用并存储到全局引用中,然后到另外一个线程去使用。
5.1.2 全局引用
全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效。同局部引用一样,全局引用也会阻止它所引用的对象被GC回收。
与局部引用可以被大多数JNI函数创建不同,全局引用只能使用一个JNI函数创建:NewGlobalRef。下面这个版本的MyNewString演示了怎么样使用一个全局引用:
/ 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 /
}
/ 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 */
}
}
...
}
上面这段代码中,一个由FindClass返回的局部引用被传入NewGlobalRef,用来创建一个对String类的全局引用。删除localRefCls后,我们检查NewGlobalRef是否成功创建stringClass。
5.1.3 弱引用
弱引用使用NewGlobalWeakRef创建,使用DeleteGlobalWeakRef释放。与全局引用类似,弱引用可以跨方法、线程使用。与全局引用不同的是,弱引用不会阻止GC回收它所指向的VM内部的对象。
在MyNewString中,我们也可以使用弱引用来存储stringClass这个类引用,因为java.lang.String这个类是系统类,永远不会被GC回收。
当本地代码中缓存的引用不一定要阻止GC回收它所指向的对象时,弱引用就是一个最好的选择。假设,一个本地方法mypkg.MyCls.f需要缓存一个指向类mypkg.MyCls2的引用,如果在弱引用中缓存的话,仍然允许mypkg.MyCls2这个类被unload:
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和它所在的本地代码在被使用时,MyCls2这个类出现先被unload,后来又会preload的情况。
当然,真的发生这种情况时(MyCls和MyCls2的生命周期不同),我们必须检查缓存过的弱引用是指向活动的类对象,还是指向一个已经被GC给unload的类对象。下一节将告诉你怎么样检查弱引用是否活动。
5.1.4 引用比较
给定两个引用(不管是全局、局部还是弱引用),你可以使用IsSameObject来判断它们两个是否指向相同的对象。例如:
(env)->IsSameObject(env, obj1, obj2)
如果obj1和obj2指向相同的对象,上面的调用返回JNI_TRUE(或者1),否则返回JNI_FALSE(或者0)。
JNI中的一个引用NULL指向JVM中的null对象。如果obj是一个局部或者全局引用,你可以使用(env)->IsSameObject(env, obj, NULL)或者obj == NULL来判断obj是否指向一个null对象。
在这一点儿上,弱引用有些有同,一个NULL弱引用同样指向一个JVM中的null对象,但不同的是,在一个弱引用上面使用IsSameObject时,返回值的意义是不同的:
(env)->IsSameObject(env, wobj, NULL)
上面的调用中,如果wobj已经被回收,会返回JNI_TRUE,如果wobj仍然指向一个活动对象,会返回JNI_FALSE。
5.2 释放引用
每一个JNI引用被建立时,除了它所指向的JVM中的对象以外,引用本身也会消耗掉一个数量的内存。作为一个JNI程序员,你应该对程序在一个给定时间段内使用的引用数量十分小心。短时间内创建大量不会被立即回收的引用会导致内存溢出。
5.2.1 释放局部引用
大部分情况下,你在实现一个本地方法时不必担心局部引用的释放问题,因为本地方法被调用完成后,JVM会自动回收这些局部引用。尽管如此,以下几种情况下,为了避免内存溢出,JNI程序员应该手动释放局部引用:
1、 在实现一个本地方法调用时,你需要创建大量的局部引用。这种情况可能会导致JNI局部引用表的溢出,所以,最好是在局部引用不需要时立即手动删除。比如,在下面的代码中,本地代码遍历一个大的字符串数组,每遍历一个元素,都会创建一个局部引用,当对这个元素的遍历完成时,这个局部引用就不再需要了,你应该手动释放它:
for (i = 0; i < len; i++) {
jstring jstr = (env)->GetObjectArrayElement(env, arr, i);
... / process jstr /
(env)->DeleteLocalRef(env, jstr);
}
2、 你想写一个工具函数,这个函数被谁调用你是不知道的。4.3节中的MyNewString演示了怎么样在工具函数中使用引用后,使用DeleteLocalRef删除。不这样做的话,每次MyNewString被调用完成后,就会有两个引用仍然占用空间。
3、 你的本地方法不会返回任何东西。例如,一个本地方法可能会在一个事件接收循环里面被调用,这种情况下,为了不让局部引用累积造成内存溢出,手动释放也是必须的。
4、 你的本地方法访问一个大对象,因此创建了一个对这个大对象的引用。然后本地方法在返回前会有一个做大量的计算过程,而在这个过程中是不需要前面创建的对大对象的引用的。但是,计算过程,对大对象的引用会阻止GC回收大对象。
在下面的程序中,因为预先有一个明显的DeleteLocalRef操作,在函数lengthyComputation的执行过程中,GC可能会释放由引用lref指向的对象。
5.2.2 管理局部引用
JDK提供了一系列的函数来管理局部引用的生命周期。这些函数包括:EnsureLocalCapacity、NewLocalRef、PushLocalFrame、PopLocalFrame。
JNI规范中指出,VM会确保每个本地方法可以创建至少16个局部引用。经验表明,这个数量已经满足大多数不需要和JVM中的内部对象有太多交互的本地方法。如果真的需要创建更多的引用,本地方法可以通过调用EnsureLocalCapacity来支持更多的局部引用。在下面的代码中,对前面的例子做了些修改,不考虑内存因素的情况下,它可以为创建大量的局部引用提供足够的空间。
· /* 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函数对允许程序员创建作用范围层层嵌套的局部引用。例如,我们可以把上面的代码重写:
· #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个或PushLocaFrame和EnsureLocalCapacity调用设置的个数。VM可能会尝试分配足够的内存,但不能够保证分配成功。如果失败,VM会退出。
5.2.3 释放全局引用
当你的本地代码不再需要一个全局引用时,你应该调用DeleteGlobalRef来释放它。如果你没有调用这个函数,即使这个对象已经没用了,JVM也不会回收这个全局引用所指向的对象。
当你的本地代码不再需要一个弱引用时,应该调用DeleteWeakGlobalRef来释放它,如果你没有调用这个函数,JVM仍会回收弱引用所指向的对象,但弱引用本身在引用表中所占的内存永远也不会被回收。
5.3 管理引用的规则
前面已经做了一个全面的介绍,现在我们可以总结一下JNI引用的管理规则了,目标就是减少内存使用和对象被引用保持而不能释放。
通常情况下,有两种本地代码:直接实现本地方法的本地代码和可以被使用在任何环境下的工具函数。
当编写实现本地方法的本地代码时,当心不要造成全局引用和弱引用的累加,因为本地方法执行完毕后,这两种引用不会被自动释放。
当编写一个工具函数的本地代码时,当心不要在函数的调用轨迹上面遗漏任何的局部引用,因为工具函数被调用的场合是不确定的,一旦被大量调用,很有可能造成内存溢出。
编写工具函数时,请遵守下面的规则:
1、 一个返回值为基本类型的工具函数被调用时,它决不能造成局部、全局、弱引用不被回收的累加。
2、 当一个返回值为引用类型的工具函数被调用时,它除了返回的引用以外,它决不能造成其它局部、全局、弱引用的累加。
对工具函数来说,为了使用缓存技术而创建一些全局引用或者弱引用是正常的。
如果一个工具函数返回一个引用,你应该详细说明返回的引用的类型,以便于调用者更好地管理它们。下面的代码中,频繁地调用工具函数GetInfoString,我们需要知道GetInfoString返回的引用的类型,以便于在每次使用完成后可以释放掉它:
· 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. /
· }
函数NewLocalRef有时被用来确保一个工具函数返回一个局部引用。为了演示这个用法,我们对MyNewString函数做了一些修改。下面的版本把一个被频繁调用的字符串“CommonString” 缓存在了全局引用里:
· 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;
· }
在管理局部引用的生命周期中,Push/PopLocalFrame是非常方便的。你可以在本地函数的入口处调用PushLocalFrame,然后在出口处调用PopLocalFrame,这样的话,在函数对中间任何位置创建的局部引用都会被释放。而且,这两个函数是非常高效的,强烈建议使用它们。
如果你在函数的入口处调用了PushLocalFrame,记住在所有的出口(有return出现的地方)调用PopLocalFrame。在下面的代码中,对PushLocalFrame的调用只有一次,但对PopLocalFrame的调用却需要多次。
· 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的第二个参数的用法。局部引用result一开始在PushLocalFrame创建的当前frame里面被创建,而把result传入PopLocalFrame中时,PopLocalFrame在弹出当前的frame前,会由result生成一个新的局部引用,再把这个新生成的局部引用存储在上一个frame当中。
网友评论