JNI技术解析第五章 局部引用和全局引用

由于本章篇幅太长,需要花费几天的时间才能介绍完。下面部分是05/18/15完成的。

JNI使用不透明引用来使用对象、数组(jobject、jclass、jstring、jarray等)。不透明引用屏蔽了JNI内部数据结构的实现细节,让程序员无需了解Java虚拟机中JNI的实现细节。原生代码不能直接通过不透明引用访问引用指向的内容,必须通过JNI函数来访问。

在JNI中,有三种不同的不透明引用:

1.局部引用、全局引用和弱全局引用

2.局部引用在原生代码离开其作用于后被自动释放(很像C/C++中的局部变量,离开其作用域后自动销毁),全局引用和弱全局引用必须由程序员手动显式释放(很像C中的malloc与free,C++中的new与delete)

3.局部引用和全局引用在其有效期间,其指向的对象可以不被垃圾回收机制回收。而弱全局引用在其有效期间则允许垃圾回收机制回收其指向的对象

4.并不是所有的引用都能在任何上下文中被使用。比如当创建一个局部引用的原生代码返回值后,该局部引用就不能继续被使用了,否则不合法。

本章对这三种引用着重介绍。

5.1 局部引用

绝大多数(注意是绝大多数)JNI函数返回局部引用。比如第四章提到的NewObject()函数就创建一个未初始化的对象并把这个对象的引用当做局部引用返回。局部引用只在创建它的动态上下文中有效,一旦这个原生函数返回,局部引用就被Java虚拟机自动释放,不再有效。因此,不能在原生函数中使用静态变量缓存局部引用的值(第四章提到的缓存ID的第一种方法),并想在接下来的调用中继续使用。比如,下面这个程序是对第四章中实现的MyNewString原生函数的部分修改,经过修改过后,这个程序就不对了。

jstring MyNewString(JNIEnv env, jchar chars, jint len)
{
/注意,原来的定义为jclass stringClass;修改为静态变量, 用来保存FindClass()函数返回的局部引用,这是错误的。/
static jclass stringClass = NULL;
/注意,原来定义为static jmethodID cid = NULL,将其修改为非静态变量。/
jmethodID cid;
jcharArray elemArr;
jstring result;
if (stringClass == NULL) {
/用静态变量保存局部引用是非法的。/
stringClass = (env)->FindClass(env, “java/lang/String”);
if (stringClass == NULL) {
return NULL; / exception thrown /
}
}
/在这里使用缓存的局部引用可能会出错。/
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;
}


将stringClass设置为静态变量保存FindClass()函数的返回值,以期望减少FindClass()函数的调用次数,这种做法是非法的。因为FindClass()函数返回局部引用,在MyNewString()函数返回时,会自动释放在这期间创建的局部引用,当然包括stringClass。此时stringClass指向的对象会被释放,使得stringClass的值无效。如果再次调用MyNewString()函数的话,很可能会引用这个无效的值,造成程序崩溃。

5.1.1 与第四章的MyNewString()函数对比

对比一下第四章的MyNewString()函数,会发现仅仅将static jmethodID cid = NULL;改为static jclass stringClass = NULL;即把methodID静态变量改变为非静态的,而将stringClass由非静态改为了静态。同样为静态局部变量,既然jmethodID都可以使用静态变量缓存,那为什么jclass不能被缓存呢?注意,我们这里限定的不能使用静态变量缓存引用类型,jclass是引用类型,但jmethodID不是。为了解决这个问题,我专门到Oracle的JNI介绍网站上查了,网址为:JNI类型介绍

JNI中引用类型间下图:

jni_reference

其中jclass、jstring、jarray等均继承了jobject,是引用类型。

而jmethodID得说明如下图:

jmethodID

说的很清楚,方法和成员变量ID均为普通的C指针类型。

因此,第四章的MyNewString()函数将指针值存储在静态变量中,Java虚拟机的垃圾回收机制不会管理C指针类型的数据。这些指针指向的对象只有在整个Java程序运行完退出进程时才被撤销。而JNI得引用类型与之不同,会被Java垃圾回收机制根据情况自动回收,所以不允许缓存局部引用到静态变量中。

5.1.2 释放局部引用

有两种方法可以释放局部引用,使其无效。第一种方法就是创建该局部引用的原生函数返回时系统自动释放所有的局部引用,第二种方法是显式调用JNI的DeleteLocalRef()函数释放局部引用。既然系统能够自动释放局部引用,那第二种方法有存在的必要吗?有!首先,我们可以自主控制局部引用的生存周期。其次,在我们不再需要局部引用的时候,可以让Java虚拟机的垃圾回收机制及时的回收该局部引用指向的对象所占用的内存空间。

下面用一个例子来说明局部引用的细节:

javaClass是Java类,类中调用了原生函数javaClass.f(),在javaClass.f()中调用了原生函数MyNewString(),而MyNewString()函数又调用了NewObject()函数产生一个jstring对象。调用关系简单描述为javaClass->f()->MyNewString()->NewObject()。经过调用并返回后,javaClass获得了一个java.lang.String类型的对象。具体的返回过程描述如下:

1.NewObject()函数是这次调用的最底层函数,它创建一个未初始化的jstring对象,并将其引用作为局部引用返回给MyNewString()。

2.MyNewString()函数将得到的局部引用又返回给javaClass.f()。

3.javaClass.f()将得到的局部引用返回给Java虚拟机。

4.Java虚拟机收到原生代码返回的局部引用后,把该引用指向的底层的java.lang.String类型对象(即NewObject()创建的jstring对象)传递给javaClass类,并释放掉这个局部引用。

直到第4步,NewObject()返回的局部引用才被虚拟机自动释放掉。可以看出,局部引用可以在同一个线程的多个原生函数之间传递,从这个函数传递到那个函数期间,局部引用并不失效,垃圾回收机制也不会回收它。直到它不再被使用的时候,垃圾回收机制才主动释放它

需要注意的是,局部引用必须在同一个线程中使用,不能够将局部引用的值保存在全局变量中,期望在其他线程中可以正确使用这个局部引用。

5.2 全局引用

全局引用可以在一个原生函数多次调用期间一直保持有效,且在多个线程之间保持有效,与局部引用一样,在它有效期间,不允许垃圾回收机制回收它,除非程序员显式调用DeleteGlobalRef()释放它。绝大多数的JNI函数返回局部引用,但只有一个JNI函数返回全局引用——NewGlobalRef()。下面我们把上一小节提到的错误的MyNewString()版本做一下修改,就能使其变得正确:

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;//抛出异常,立即返回。
}
/
创建一个全局引用,保存局部引用的值,使其全局有效/
stringClass = (
env)->NewGlobalRef(env, localRefCls);
/不再需要局部引用了,显式释放掉它/
(env)->DeleteLocalRef(env, localRefCls);
/
判断全局引用是否创建成功/
if (stringClass == NULL) {
return NULL; /
全局引用创建失败,抛出out of memory异常*/
}
}
…//省略
}





在这个修改版本中,我们把jclass局部引用转换为全局引用,并把不再使用的局部引用显式释放掉,及时让垃圾回收机制回收局部引用的内存。这样一来,jclass的值全局有效,Java虚拟机不再主动的释放它,必须由程序员调用JNI函数DeleteGlobalRef()函数释放它。

5.3 弱全局引用

弱全局引用是Java 2 SDK 发行版1.2新推出的引用。使用NewGlobalWeakRef()函数创建它,使用DeleteGlobalWeakRef()函数释放它。弱全局引用也可以在多次原生函数的调用期间保持有效,且在多线程之间保持有效,但它不保证它所指向的对象不会被垃圾回收机制回收

在上一版本的MyNewString()函数中,我们使用了全局引用。也可以用弱全局引用替换它,对于java.lang.String类来说,全局引用与弱全局引用没有区别,因为String类是系统类,垃圾回收机制永远不会回收它。

那弱全局引用有什么用处呢?它主要用于在原生函数中被缓存的引用所指向的对象在需要的时候能够被垃圾回收机制回收(即使这个弱全局引用的对象还在被使用,也能够被回收。而局部引用和全局引用则是无论什么情况,只要引用还被使用,其对象就不能被回收)。听起来很绕,我们举一个例子说明一下。

有一个Java包为mypkg,保重包含了两个Java类:javaClass和javaClass2。javaClass调用了原生函数javaClass.f(),在f()中保存了javaClass2类的引用为弱全局引用,这种情况下,javaClass2类仍然可以被卸载(unload):

JNIEXPORT void JNICALL Java_mypkg_javaClass_f(JNIEnv env, jobject self)
{
static jclass myCls2 = NULL;
if (myCls2 == NULL) {
jclass myCls2Local =(
env)->FindClass(env, “mypkg/javaClass2”);
if (myCls2Local == NULL) {
return; / 找不到相应的类 /
}
myCls2 = NewWeakGlobalRef(env, myCls2Local);
if (myCls2 == NULL) {
return; / out of memory异常 /
}
}
… / 使用javaClass2类的弱全局引用myCls2的代码 /
}





假定javaClass与javaClass2有相同的生存周期(比如它们被同一个类加载器加载),此时我们不会考虑下面的情况:javaClass2被卸载并在稍后被重新加载,与此同时javaClass和javaClass.f()却仍然在使用。如果这种情况发生了,我们可以使用下一小节介绍的JNI函数来检查弱全局引用到底是指向一个仍然活动的类对象还是指向已经被垃圾回收了的类对象。

5.4 比较引用

对任意的两个引用(局部引用、全局引用和弱全局引用),调用JNI的IsSameObject()函数来判断这两个引用是否指向同一个对象:

(*env)->IsSameObject(env, obj1, obj2);





如果引用obj1和obj2指向同一个对象,该函数返回JNI_TRUE(或1),否则返回JNI_FALSE(或0)。

在JNI中,NULL引用指向Java虚拟机中的null对象,如果obj是局部引用或全局引用,那么可以使用

 (*env)->IsSameObject(env, obj, NULL)





obj == NULL





其中任意一个来判断obj是否指向null对象。

但是这两种方法对于弱全局引用的作用有些不同。NULL弱全局引用仍然指向null对象,但IsSameObject()函数对弱全局引用有特殊作用。假定wobj是一个非NULL的弱全局引用,那么如下调用:

(*env)->IsSameObject(env, wobj, NULL)





如果wobj指向了一个已经被回收了的无效的对象时,返回JNI_TRUE,如果wobj指向一个仍然活动的对象时返回JNI_FALSE。也就是说,可以使用IsSameObject()函数判断非NULL的弱全局引用是否指向一个仍然活动的对象。上一小节中的代码就可以使用这个函数进行判断,如果弱全局引用myCls2仍然指向一个活动的对象,那么首先释放掉这个对象,再卸载javaClass2。如果myCls2指向的对象已经被回收了,那么就可以直接卸载javaClass2了。

5.5 释放引用

除了被引用的对象外,每个JNI函数本身也占据了一定的内存空间。作为一个JNI程序员,要始终清除你得原生代码在一个给定时间使用的引用数量。特别需要注意,虽然局部引用在原生代码返回时会自动被回收,但你仍必须清楚原生代码在其运行期间最多能够创建和使用的局部引用的数量。不管局部引用使用有多么短暂,创建过多的局部引用肯定会导致内存耗尽

5.5.1 释放局部引用

多数情况下,你不必考虑显式释放局部引用,因为系统会在合适的时候主动释放它。但某些特殊情况下,就必须要程序员显式释放了:

1.在一个原生函数中创建了大量的局部引用,多见于在循环中一直创建引用。此时,释放掉不再需要的引用就显得非常的重要,否则会导致JNI内部的局部引用表溢出。如下面的例子所示,在原生代码段中的循环里,每一次循环都会创建局部引用,为了避免过多的内存占用,可以在每一次循环结束时显式的释放掉不再需要的局部引用:

for (i = 0; i < len; i++) {
jstring jstr = (env)->GetObjectArrayElement(env, arr, i);
… /
对jstr进行处理的代码 /
(
env)->DeleteLocalRef(env, jstr);//及时释放掉不再使用的局部引用
}





2.你写的原生函数是作为公用的工具函数,这类工具函数可以被任何其他函数调用,因此调用它的上下文环境往往各不相同,比如第四章实现了的MyNewString()代码如下:

jstring MyNewString(JNIEnv env, jchar chars, jint len)
{
jclass stringClass; //局部引用
jmethodID cid;
jcharArray elemArr;//局部引用
jstring result;//局部引用
stringClass = (env)->FindClass(env, “java/lang/String”);
if (stringClass == NULL) {
return NULL; /
exception thrown /
}
/
获得String(char[])的构造函数的ID /
cid = (
env)->GetMethodID(env, stringClass,”<init>”, “([C)V”);
if (cid == NULL) {
return NULL; / 抛出异常 /
}

 /* 创建包含字符串元素的数组 */
 elemArr = (*env)-&gt;NewCharArray(env, len);
 if (elemArr == NULL) {
     return NULL; /* 抛出异常 */
 }
 (*env)-&gt;SetCharArrayRegion(env, elemArr, 0, len, chars);
 /* 创建java.lang.String对象 */
 result = (*env)-&gt;NewObject(env, stringClass, cid, elemArr);
 /* 显式释放局部引用 */
<span style="color: #ff0000;"> (*env)-&gt;DeleteLocalRef(env, elemArr);
 (*env)-&gt;DeleteLocalRef(env, stringClass);</span>
 return result;

}





在这个函数中,定义了3个局部引用:jclass、jcharArray和jstring。其中,函数把jstring作为返回值,因此jstring这个局部引用不能被释放(如果被释放了,那底层的对象就被回收了,这个函数的功能就丧失了)。而另外两个局部引用是为了获得jstring才创建的中间变量。如果对MyNewString()函数的定位是一般的原生函数,那么最后的显式释放两个局部引用是可以省略的,因为系统会在这个函数返回时自动释放局部引用。然而,如果该函数是作为公用的工具函数,那么最后的显式释放语句就显得格外重要。如果没有释放这两个无用的局部引用,那么任何其他函数在调用MyNewString()函数期望获得一个jstring类对象的引用,同时他们也会获得两个无用的中间局部引用jclass和jcharArray。直到MyNewString的调用者返回后,jclass、jcharArray和jstring这三个局部引用才会被释放。如果调用MyNewString()的上下文非常多(假如在很短的一段时间内有十万次调用),那么虚拟机就要在这段时间内同时保存30万个局部引用,内存肯定会被消耗完。

所以,在公用的工具函数中,调用它的上下文未知,就必须显式的删除掉所有的无用的局部引用

3.如果你写的原生代码本来就不打算返回(比如想要其作为一种后台服务,无限循环等待事件并处理事件),那么就必须显式的删除循环内的所有无用的局部引用,以免浪费内存资源。

4.你的原生函数首先对一个非常庞大的对象创建了局部引用,在使用完这个引用后,又做了其他的一些处理。在做其他处理的时候,虽然这个庞大对象的局部引用不再需要,但系统并不会自动回收,直到这个原生函数返回。因此,程序员需要手动显式的释放局部引用。

5.5.2 在Java 2 SDK 发行版1.2中管理局部引用

Java SDK 1.2 为管理局部引用的生存周期提供了额外的JNI函数:EnsureLocalCapacity(), New- LocalRef(), PushLocalFrame()和 PopLocalFrame()

通常情况下,Java虚拟机允许每个原生函数至少创建16个局部引用,而且根据经验,16个局部引用足够绝大多数的情况。然而,如果需要使用更多的局部引用,就必须调用EnsureLocalCapacity()函数确保空间足够。比如说,对5.5.1节中1的代码做如下修改:

/ 允许创建的局部引用数量与数组的长度(len)一致 /
if ((env)->EnsureLocalCapacity(env, len)) < 0) {
… / 空间不足,抛出异常 /
}
/
向Java虚拟机申请了足够的局部引用的数量/
for (i = 0; i < len; i++) {
jstring jstr = (
env)->GetObjectArrayElement(env, arr, i);
… / 对jstr处理的代码 /
/ 此时在循环中就不需要调用DeleteLocalRef()函数了 /
}





当然,这个版本比修改前的版本占用更多的内存。

可以使用PushLocalFrame()和 PopLocalFrame()函数重写上一版本的循环:

#define N_REFS … / 定义每次循环最多可以创建多少个局部引用 /
for (i = 0; i < len; i++) {
/在一次循环中申请N_REFS个局部引用的数量/
if ((env)->PushLocalFrame(env, N_REFS) < 0) {
… /
out of memory异常 /
}
jstr = (
env)->GetObjectArrayElement(env, arr, i);
… / 处理jstr的代码 /

/循环结束,在这一次的循环中创建的所有局部引用都会被释放掉。/
(*env)->PopLocalFrame(env, NULL);

}





PushLocalFrame()和 PopLocalFrame()函数确定了一个范围,使用PushLocalFrame()函数申请在这个范围内可以创建的局部引用数量N,如果成功,则在该范围内可以创建最多N个局部引用,使用PopLocalFrame()函数释放掉这个范围内创建的所有的局部引用。使用这一对函数的好处是,我们可以批量的释放局部引用。

NewLocalRef()函数的使用会在后面的小节中提到。

使用EnsureLocalCapacity()、PushLocalFrame()和 PopLocalFrame()函数对可以申请超出16个局部引用数量。Java虚拟机尽量根据你申请的数量分配内存,但不一定会分配成功,一旦分配失败,虚拟机就会退出。所以,程序员应该保证有足够的内存空间,并且及时释放不再需要的局部引用以保证这种情况不会发生。

Java SDK 1.2提供了命令行参数:-verbose:jni。如果这个命令行参数被启动,那么虚拟机就会对超出16个局部引用的函数给出相关的报告。

5.5.3 释放全局引用

使用DeleteGlobalRef()函数显式释放全局引用如果释放失败,那么这个引用的对象永远不会被回收,会造成内存泄露使用DeleteGlobalWeakRef()函数显式释放弱全局引用,如果释放失败,虚拟机仍然会回收其对象,但弱全局引用本身占用的内存就不能再使用了,也会造成内存泄露,不过没有全局引用造成的内存泄露那么严重。

下面的部分是05/23/2015完成的。

5.6 管理引用的规则

在原生代码中管理引用可以减少不必要的内存浪费,这一小节就介绍在原生代码中管理引用的规则。

通常,有两类原生代码:使用原生代码实现的函数或共享共用的工具函数。

本章的前面也简单提到过这两类原生代码在管理引用时的区别。首先要搞清楚这两类原生代码分别是什么。

第一类函数是使用原生代码实现的函数:这类函数使用原生代码完成项目中Java不能完成或完成起来很困难的任务,它是跟项目有关系的,是项目的一部分。

第二类函数是共享共用的工具函数:这类函数是用原生代码编写一些工具函数,不属于任何的项目。它可以在其他任何地方被调用,类似于操作系统的API,可以在任何地方被调用。比如我们对Linux的write()函数做一些错误处理和封装操作,形成了我们自己的工具函数my_write()。

在第一类函数中,要注意的是不能在循环中一直创建局部引用而在循环结束时不释放局部引用,在根本不返回的原生函数中(例如该函数作为一个服务一直运行在后台,不会返回,直到杀死其进程才会退出)也要注意及时释放局部引用。如果函数中的局部引用少于16个,那么可以不必手动释放,在函数返回时由Java虚拟机自动释放。对于全局引用和弱全局引用,必须手动释放,不能依靠虚拟机,否则会造成内存泄露或/和不必要的内存浪费。

在第二类函数中,必须注意局部引用要及时释放,同样的,少于16个可以交由虚拟机,但最好是自己手动释放。这是因为工具函数会在不确定的上下文中调用,编写工具函数时根本不知道调用它的上下文是什么,调用规则是什么,有可能这个工具函数会在一个循环中被重复调用很多次,因此,及时释放局部引用是有好处的。

  • 当一个返回基本数据类型的工具函数被调用时,除了返回基本类型外,这个函数不应该保留其他任何局部引用、全局引用和弱全局引用。
  • 当一个返回引用的工具函数被调用时,除了它返回的引用外,这个函数不应该保留其他任何引用。
    在第二类函数中,可以创建少量的全局引用或弱全局引用用于缓存,但要保证创建这些缓存的代码只在第一次调用该函数时被执行,后续的调用不再执行,减少执行时间。

当一个工具函数返回引用的时候,调用者必须要知道这个函数返回的是局部引用、全局引用还是弱全局引用,否则调用者无法对引用进行管理——局部引用可以交由虚拟机或使用DeleteLocalRef()函数,全局引用可以使用DeleteGlobalRef(),弱全局引用可以使用DeleteWeakGlobalRef()。因此,在Java 2 SDK 1.2中有了一个新的函数NewLocalRef(),有了这个函数,可以保证所有的工具函数均返回局部引用。下面我们修改一下MyNewString()这个欧诺个局函数,使用NewLocalRef()使其一直返回局部引用。

下面的MyNewString()函数版本中使用全局引用缓存了一个jstring变量,其内容为”CommonString”:

jstring MyNewString(JNIEnv env, jchar chars, jint len)
{
static jstring result;
/ wstrncmp()函数比较两个Unicode字符串,相等返回0 /
if (wstrncmp(“CommonString”, chars, len) == 0) {
/ cachedString用于指向”CommandString” /
static jstring cachedString = NULL;
if (cachedString == NULL) {
/ 如果引用为空,那么在第一次的时候创建这个引用 /
jstring cachedStringLocal = … ;
/ 把上面的引用转换为全局引用缓存起来,后续调用可以直接使用 /
cachedString = (env)->NewGlobalRef(env, cachedStringLocal);
}
return (
env)->NewLocalRef(env, cachedString);
}
… / 把chars创建为jstring(局部引用),返回这个局部引用 /
return result;
}





上面的代码中可以看到,调用MyNewString()时,传递了一个字符串chars和这个字符串的长度len。如果chars与”CommonString”相等的话,只需要在第一次调用这个函数时创建”CommonString”的全局引用,后续的调用可以直接使用这个全局引用而不需要再创建。如果chars与”CommonString”不相等的话,就必须创建chars的局部引用并返回。在返回”CommonString”的引用时,我们必须把用于缓存的全局引用转换为要返回的局部引用,因此使用了NewLocalRef()函数,这样,不管chars与”CommonString”是否相等,MyNewString()函数始终返回局部引用。这样,MyNewString()函数的调用者就可以用管理局部引用的方式管理这个函数返回的引用。

前面提到的Push/PopLocalFrame()函数对在管理局部引用时非常的高效便捷,只需要在原生函数的开始调用PushLocalFrame(),在原生代码返回前调用PopLocalFrame(),那么这对函数间创建的所有局部引用都会被释放,因此强烈推荐使用Push/PopLocalFrame()。请注意一点,如果在原生函数开始时使用了PushLocalFrame(),那么必须在原生函数可能的返回地点前调用PopLocalFrame(),比如:

jobject f(JNIEnv env, …)
{
jobject result;
if ((
env)->PushLocalFrame(env, 10) < 0) {
/ 如果出错,那么无需调用PopLocalFrame(),因为还没有创建任何的局部引用 /
return NULL;
}
…//其他原生代码
result = …;//创建局部引用
if (…) {
/ 在返回之前必须使用PopLocalFrame() /
result = (env)->PopLocalFrame(env, result);
return result;
}
…//其他原生代码
/
在正常返回的时候也要调用PopLocalFrame() /
result = (
env)->PopLocalFrame(env, result);
return result;
}





如果在函数返回前忘记了使用PopLocalFrame(),这种后果是未定义的,可能会造成虚拟机崩溃。

在上述代码中也证实了给PopLocalFrame()函数传递第二个参数有时是非常有用的。在PushLocalFrame()函数调用成功后产生了一个新的frame,在这个frame中创建了局部引用result。调用PopLocalFrame()则会把最顶层的frame销毁(这个frame中的局部引用也就会被释放)。如果result不作为PopLocalFrame()的第二个参数传入,那么result就会被销毁,那return result;这条语句返回的引用是空的,结果是未知的。然而,如果把result作为PopLocalFrame()的第二个参数,那么在销毁最顶层的frame之前,PopLocalFrame()函数会把这个frame中的result局部引用转换为一个新的局部引用,这个新的局部引用不在要销毁的frame中,因此就可以安全的返回result了。

本章的内容是关于引用的管理的,到此就讲完了,有问题可以留言或发邮件:xinspace@yeah.net