JNI 提供了一些实例和数组类型(jobject、jclass、jstring、jarray 等)作为 不透明的引用供本地代码使用。本地代码永远不会直接操作引用指向的 VM 内部 的数据内容。要进行这些操作,必须通过使用 JNI 操作一个不引用来间接操作数 据内容。因为只操作引用,你不必担心特定 JVM 中对象的存储方式等信息。这样 的话,你有必要了解一下 JNI 中的几种不同的引用:
1、 JNI 支持三种引用:局部引用、全局引用、弱全局引用(下文简称“弱 引用”)。
2、 局部引用和全局引用有不同的生命周期。当本地方法返回时,局部引用 会被自动释放。而全局引用和弱引用必须手动释放。
3、 局部引用或者全局引用会阻止 GC 回收它们所引用的对象,而弱引用则 不会。
4、 不是所有的引用可以被用在所有的场合。例如,一个本地方法创建一个 局部引用并返回后,再对这个局部引用进行访问是非法的。
大多数 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, "" , "([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 回收。 局部引用只在创建它们的线程中有效,跨线程使用是被禁止的。不要在一个线程 中创建局部引用并存储到全局引用中,然后到另外一个线程去使用。
全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效。同局部引用一 样,全局引用也会阻止它所引用的对象被 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。
弱引用使用 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 的类 对象。下一节将告诉你怎么样检查弱引用是否活动。
给定两个引用(不管是全局、局部还是弱引用),你可以使用 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。
每一个 JNI 引用被建立时,除了它所指向的 JVM 中的对象以外,引用本身也会消 耗掉一个数量的内存。作为一个 JNI 程序员,你应该对程序在一个给定时间段内 使用的引用数量十分小心。短时间内创建大量不会被立即回收的引用会导致内存 溢出。
大部分情况下,你在实现一个本地方法时不必担心局部引用的释放问题,因为本 地方法被调用完成后,JVM 会自动回收这些局部引用。尽管如此,以下几种情况 下,为了避免内存溢出,JNI 程序员应该手动释放局部引用:
1、 在实现一个本地方法调用时,你需要创建大量的局部引用。这种情况可
能会导致 JNI 局部引用表的溢出,所以,最好是在局部引用不需要时立即手 动删除。比如,在下面的代码中,本地代码遍历一个大的字符串数组,每遍 历一个元素,都会创建一个局部引用,当对这个元素的遍历完成时,这个局 部引用就不再需要了,你应该手动释放它:
for (i = 0; i < len; i++) {
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* process jstr */
(*env)->DeleteLocalRef(env, jstr);
}
2、MyNewString 演示了怎么样在工具函数中使用引用后,使用 DeleteLocalRef 删除。不这样做的话,每次 MyNewString 被调用完成后,就会有两个引用仍 然占用空间。
3、 你的本地方法不会返回任何东西。例如,一个本地方法可能会在一个事 件接收循环里面被调用,这种情况下,为了不让局部引用累积造成内存溢出, 手动释放也是必须的。
4、 你的本地方法访问一个大对象,因此创建了一个对这个大对象的引用。 然后本地方法在返回前会有一个做大量的计算过程,而在这个过程中是不需 要前面创建的对大对象的引用的。但是,计算过程,对大对象的引用会阻止 GC 回收大对象。
在下面的程序中,因为预先有一个明显的 DeleteLocalRef 操作,在函数 lengthyComputation 的执行过程中,GC 可能会释放由引用 lref 指向的对象。
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 非常有用,我们会在后面演示。 本地代码可能会创建大量的局部引用,其数量可能会超过 16 个或 PushLocaFrame
和 EnsureLocalCapacity 调用设置的个数。VM 可能会尝试分配足够的内 存,但不能够保证分配成功。如果失败,VM 会退出。
当你的本地代码不再需要一个全局引用时,你应该调用 DeleteGlobalRef 来释放 它。如果你没有调用这个函数,即使这个对象已经没用了,JVM 也不会 回收这个全局引用所指向的对象。
当你的本地代码不再需要一个弱引用时,应该调用 DeleteWeakGlobalRef 来释放 它,如果你没有调用这个函数,JVM 仍会回收弱引用所指向的对象,但 弱引用本身在引用表中所占的内存永远也不会被回收。
前面已经做了一个全面的介绍,现在我们可以总结一下 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 当中。