1 前言
1.1 什么是引用
在使用JNI技术进行Java和Native代码的混编的时候,不可避免的要大量使用JNI提供的一些实例和数组类型(jobject、jclass、jstring、jarray等),而对这些类型的操作,往往不是通过该类类型变量本身进行的,而是通过使用JNI操作一个不透明的引用来间接的操作数据内容。因为只操作引用而不牵涉类型具体本身的内容,所以不必担心依赖于特定Java虚拟机实现的内部对象分布。这一机制无疑提高了程序运行效率,也增加了我们的学习内容。引用的产生随处可见,常见的,如FindClass
和 NewObject
等函数都会自动产生一个指向JVM内部数据的引用。
1.2 为什么要使用引用
引用这个机制在JNI编程中广泛出现,我们必须了解其工作原理和使用规范,才能够更好的去使用这一机制,进而保证我们的代码更加健壮、高效。尤其是在频繁的JNI交互的缓存技术中,使用好引用机制,有利于减少不必要的引用累积、不安全的引用访问以及更好的内存管理,保障程序不会在JNI内部崩溃。
本文主要参考了《JNI编程规范》一书中第五章节的内容进行整理和学习。
2. 全局引用和局部引用
JNI支持三种不同的引用:局部引用(local references)、全局引用(global references)和弱全局引用(weak global references)
- 局部引用和全局引用的生命周期不同,当本地方法返回时,局部引用会被自动释放,而全局引用和弱全局引用则需要手动释放。
- 局部引用和全局引用会阻止GC(Garbage collection)回收它们所引用的对象(注意是引用的对象,不是引用本身),但是弱引用不会。
2.1 局部引用
大多数JNI函数会创建局部引用,例如,NewObject
函数创建一个对象实例并返回这个对象的局部引用。局部引用只有在创建它的本地方法返回之前有效,本地方法返回之后,局部引用会自动释放(这一点类似于局部变量的生命周期)。因此,不能在局部方法中把局部引用存储为静态变量缓存起来以供下次使用。
下面是一个局部引用的错误使用例子:
/* 以下代码存在错误 */
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; /* 抛出异常 */
}
}
/* 此处使用缓存的变量stringClass可能错误,
因为局部引用已被释放,此时的stringClass内容非法*/
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变量的目的,是能够消除反复调用FindClass
函数带来的开销,但这种方法不正确,因为FindClass
函数返回了一个java.lang.String
的对象的引用,假定存在如下调用,方法C.f
调用了MyNewString
函数:
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(); // 第一次调用时或许正确
... = C.f(); // 第二次调用时存在错误
...
有两种方法释放局部引用,一种是如上所言,VM在本地方法返回后会自动的释放,另一种就是使用DeleteLocalRef
函数手动释放。
这里会有一个疑问,既然VM会自动释放已经失效的局部引用,那么这个函数的作用是什么呢?
答案是,DeleteLocalRef
函数能够显式的释放局部引用之外,还可以手动释放引用所指向的对象,在VM把局部应用回收之前,局部引用会保持所指向的对象不被GC回收,直到VM失效为止,执行DeleteLocalRef
函数能够立即释放其所指向的对象。
局部引用可以在多个本地函数之间传递(而非调用),如上文中的MyNewString
返回了一个NewObject
函数创建的引用,然后Java_C_f
函数又返回了该引用。
局部引用仅能在当前创建其所在的线程中使用,跨线程使用或者在其他本地方法中存储为全局变量都会造成错误。
2.2 全局引用
全局引用可以被多个本地方法调用,也可以多线程使用,全局引用需要开发者手动释放,和局部引用一样的是,它也保证在其生命周期内所指向的对象不被GC回收。
与局部引用不同的是,全局引用仅能够被一个JNI函数NewGlobalRef
创建。下面通过一段代码来演示这个过程:
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 */
}
/* 创建全局引用 */
stringClass = (*env)->NewGlobalRef(env, localRefCls);
/* 局部引用失去作用,手动销毁*/
(*env)->DeleteLocalRef(env, localRefCls);
/* Is the global reference created successfully? */
if (stringClass == NULL) {
return NULL; /* out of memory exception thrown */
}
}
...
}
在本例中,FindClass
函数创建了一个Java.lang.String
对象的局部引用,并使用NewGlobalRef
函数将其升级为全局引用(而不是直接赋值给一个全局变量,这样做会造成错误)。局部引用随即释放,节省内存空间且不影响新的全局引用的存在。
2.3 弱引用
弱引用是Java 2SDK release 1.2
版本中的新特性,它由NewGlobalWeakRef
函数创建,并由DeleteGlobalWeakRef
函数释放,与全局引用相同的是,它也可以被多个本地函数调用,支持多线程访问。与全局引用不同的是,它不会阻止GC隐式的回收引用对象。
上一个例子中,我们使用了缓存的全局引用,这个例子也可以用弱引用来进行缓存,由于Java.lang.String
对象不会被GC回收,所以我们不必关注对象是否被回收,因此,无论是使用一个全局引用还是弱引用都无伤大雅。因此,弱引用更适用于,我们不必阻止GC隐式回收引用对象的情况,或者根本不需要关注引用对象生命周期的情况。
下面通过一个例子来演示弱引用的用法,假定一个本地方法mypkg.MyCls.f
需要缓存一个指向mypkg.MyCls2
类的引用,缓存该类到一个弱引用中,能够允许该类被卸载。
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;
}
// 创建一个弱引用
myCls2 = NewWeakGlobalRef(env, myCls2Local);
if (myCls2 == NULL) {
return; /* out of memory */
}
}
...
}
我们假定MyCls
类和MyCls2
类两者具有相同的生命周期(例如,被同一个类加载器加载),因此我们不必考虑在MyCls
类在使用的时候,MyCls2
被卸载或者重载的情况。即便是这种情况可能发生, 我们也可以通过检测弱引用所指向的对象是否仍是活动的,或者已经被GC回收了。下一节讲介绍如何进行这个检测。
2.4 引用比较
通过使用IsSameObject(env,obj1,obj2)
函数可以判断,参数中给定的两个引用是否指向了同一个对象,如果相同,返回JNI_TRUE(1)
,否则JNI_FALSE(0)
。
一个NULL
的引用指向了JVM中的null object
,如果是局部引用或者全局引用,你可以使用
(*env)->IsSameObject(env, obj, NULL)
或者
obj == NULL
来判断是否指向了一个null object
弱引用会有些微不同,IsSameObject
函数对于弱引用有着特殊意义,使用该函数一般用来检测一个非空弱引用是否指向一个活动的对象。假定对于一个弱引用wobj,存在如下调用:
(*env)->IsSameObject(env, wobj, NULL)
如果弱引用指向的对象已经被GC回收,那么返回JNI_TRUE
,如果指向的是一个活动的对象,则返回JNI_FALSE
。
3 释放引用
除了引用对象所占的内存空间之外,每个JNI引用本身也会消耗一定内存空间。因此,作为开发者,你必须时刻注意在给定的时间内所使用的引用数量。特别是,你应该知道程序在执行过程中可以创建的局部引用数量的上限,即使这些局部引用最终会被JVM自动释放,但过度的引用创建,不管多么短暂,都会导致内存耗尽。
3.1 释放局部引用
大多数情况下,我们不需要关注局部引用的销毁,VM会帮我们完成这项工作,但是下面一些情况需要我们手动释放局部引用:
- 如果在一个本地方法中大量使用本地引用,这可能会导致内部JNI的局部引用溢出,随用随删是一个很好的做法,如下代码演示了一个循环中访问大量字符串数组的例子,每一轮的循环都会产生一个局部引用,如果不及时删除该引用,会极大的浪费资源:
for (i = 0; i < len; i++) {
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
...
// 随用随删
(*env)->DeleteLocalRef(env, jstr);
}
- 如果是写一个工具函数,最好也随手删掉,避免了访问已经失效的引用,如第一个
MyNewString
例子中展示的那样 - 如果本地方法不返回任何值,内部存在一个巨大的循环分支,在循环中及时释放局部引用是至关重要的,不然可能会造成引用累计,从而导致内存泄漏
- 对于引用指向的较大的对象,也需要立即删掉比较安全。
3.2 Java 2 SDK Release1.2 中的局部引用管理
Java 2 SDK Release 1.2 提供了一组额外的函数来管理局部引用的生命周期,其中包括EnsureLocalCapacity
、PushLocalFrame
和PopLocalFrame
。
- EnsureLocalCapacity函数
JNI规范要求虚拟机自动地确保每个本地方法可以创建至少16个局部引用。经验表明,这为大多数不包含与Java虚拟机中的对象的复杂交互的本地方法提供了足够的容量。但是,如果需要创建额外的局部引用,本地方法可能会发出EnsureLocalCapacity调用,以确保有足够数量的本地引用可用。
if ((*env)->EnsureLocalCapacity(env, len)) < 0) {
... /* 内存溢出 */
}
for (i = 0; i < len; i++) {
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* process jstr */
/* 由于创建了内存引用,所以不必再执行DeleteLocalRef */
}
- Push/PopLocakFrame函数
PushLocalFrame为一定数量的局部引用创建了一个使用堆栈,而PopLocalFrame负责销毁堆栈顶端的引用。Push/PopLocalFrame函数对提供了对局部引用的生命周期更方便的管理。
下面是个例子演示使用方法:
#define N_REFS ...
for (i = 0; i < len; i++) {
if ((*env)->PushLocalFrame(env, N_REFS) < 0) {
... /* 内存溢出 */
}
jstr = (*env)->GetObjectArrayElement(env, arr, i);
...
(*env)->PopLocalFrame(env, NULL);
}
3.3 释放全局引用
- 当你的本地代码不再需要一个全局引用时,你应该调用
DeleteGlobalRef
来释放它。如果你没有调用这个函数,即使这个对象已经不再使用,JVM也不会回收这个全局引用所指向的对象。 - 当你的本地代码不再需要一个弱引用时,应该调用
DeleteWeakGlobalRef
来释放它,如果你没有调用这个函数,JVM仍会回收弱引用所指向的对象,但弱引用本身在引用表中所占的内存也不会被回收。
4 总结
下面通过一张表格简单的总结一下JNI支持的三种引用的特性:
局部引用 | 全局引用 | 弱引用 | |
---|---|---|---|
自动回收 | 是 | 否 | 否 |
需要手动释放 | 否 | 是 | 是 |
阻止VM回收对象 | 是 | 是 | 否 |
创建函数 | JNI函数自动创建 | NewGlobalRef | NewWeakGlobalRef |
销毁函数 | DeleteLocalRef | DeleteGlobalRef | DeleteWeakGlobalRef |
多线程和多方法调用 | 否 | 是 | 是 |