JNI官方文档翻译5-局部和全局引用

    时间过得好快,一转眼写到了第五章,JNI对于许多java程序员来说一般是很陌生的,因为,要想使用JNI必须使用C,然而很多Java程序员都不会C,其实实际上不是这样的。项目中应该是分工协作才是,JNI作为一种粘合剂,将Java代码和C代码粘合在一起。作为一个java程序员,你至少应该会一点点C。


      JNI暴露给程序员的并不是真正的引用,而是不透明引用,文档当中称之为opaque references,就是说,我们通过这个 opaque reference 可以间接操作这个引用,但是它并不是引用。我们只能通过JNI函数来访问这个数据结构,这为我们隐藏了依赖于具体虚拟机的实现,我们需要做的只是了解这些接口,并学会如何使用即可。

      JNI提供3中类型的不透明引用,global reference  , local reference,  weak global reference (不做翻译,直接用英文名词), local ref 和global ref有不同的声明周期, local ref 是自动释放的, 而global ref 和weak global ref 直到你手动释放才会被释放。 local ref 和 global ref 持有对象引用都可以保证不被垃圾回收。weak global ref 则允许被垃圾回收。不是所有的引用都能在所有的上下文使用,在native方法返回后,再去访问local ref 是不合法的。


    我们用一系列例子来举例一下global ref 和local ref的不同。

   local ref: 大多数JNI函数创建的是 local ref, NewObject 创建的就是局部引用, local ref的作用域就是那个native函数的范围,并且就在那次函数调用有效,这种局部变量和C里的局部变量一样。native方法返回这些local ref就会被自动释放。绝对禁止该将local ref保存为static ,并且希望下次方法调用再使用这个local ref。这个类比一下C函数里的static变量,例如,下面的例子是MyNewString的修改版,当然代码是不正确的。

/* This code is illegal */
jstring MyNewString(JNIEnv *env, jchar *chars, jint len)
{
    static jclass stringClass = NULL;//我们不应该cache局部引用
    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;
}
为什么这么做是不对的: 假如C.f() 是一个native方法的调用,这个方法的实现如下:

JNIEXPORT jstring JNICALL Java_C_f(JNIEnv *env, jobject this)
{
    char *c_str = ...;
    ...
    return MyNewString(c_str);//native调用native
}

我们重复调用这个方法就可能crash, 因为stringClass引用了一个已经被释放的内存,这个道理就和C的函数里使用static方式保存一个局部变量的地址是一样的,除非你在堆中分配内存。

        管理local ref有两种方式,一种是等待函数return后虚拟机自动释放,另一种是调用JNI函数DeleteLocalRef显示释放 。 虚拟机能自动为我们释放内存,我们为什么还需要手动释放?? local ref在生命周期内是占内存的,并且关联的对象不会被垃圾回收,local ref可以在不同的native方法之间传递。局部变量是需要被销毁的,但是local ref关联的对象可以被函数返回,如果不返回,那么这个对象连同local ref 就被销毁了。

    即使local ref 可以在native方法之间传递,但是,这个传递也不是没有限制,唯一的限制就是不能跨线程。线程是local ref最后的作用域。



global ref:

使用全局引用,来修改我们的例子:这样的代码,多次调用不会有问题

/* 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");//这是局部引用 local ref
        if (localRefCls == NULL) {
            return NULL; /* exception thrown */
        }
        /* Create a global reference */
        stringClass = (*env)->NewGlobalRef(env, localRefCls);//这是全局引用 global ref
        /* 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 */
        }
    }
   ...
}



weak global ref:

是在Java 2 SDK release 1.2. 中引入的,创建NewGlobalWeakRef  释放DeleteGlobalWeakRef , global ref 和weak global ref 都可以跨线程。 和global ref不同的是,weak global ref 不保证关联的对象不被垃圾回收。上个例子,我们可以将 stringClass 用作weak global ref,因为,java.lang.String 是一个系统类,永远不会被垃圾回收。所以也就无所谓使用global ref 或者weak global ref了。

什么时候weak global ref 有用呢? 看下面例子:mypkg.MyCls.f( )

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 */
}

如果我们不保证weak global ref关联的对象不被垃圾回收,这个例子中,即使mypkg.MyCls2 的引用被声明成了weak global ref 它仍然可以被虚拟机卸载,这个系统类不同。

后面再讲述,怎么判断这个weak global ref 关联的对象是否还活着。

 

引用的比较,比较的是指向对象是否是同一个对象:

对于global ref 和local ref  weak global ref:   (*env)->IsSameObject(env, obj1, obj2)   返回JNI_TRUE JNI_FALSE

判断引用是否为空(*env)->IsSameObject(env, obj, NULL)   或者 obj == NULL, 但是对于weak global ref 情况稍有不同,假如(*env)->IsSameObject(env, wobj, NULL) , wobj是weak global ref 那么如果wobj被回收了,就会返回JNI_TRUE,返回JNI_FALSE 说明对象还活着。




释放local ref:

我们一般不需要手动释放local ref ,虚拟机会为我们自动管理,但是手动管理可以更有效的利用内存。如下情形,主动释放,会更有效的利用内存,而不会内存耗尽:

for (i = 0; i < len; i++) {
    jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
    ... /* process jstr */
    (*env)->DeleteLocalRef(env, jstr);
}
加入你的native方法不返回,而是进入一个死循环,那么主动释放就会更有效利用内存。 总之文档上举例都是native函数很久才返回的例子,我们应当保持良好的习惯,不使用的尽量手动释放,除非你确定这个native方法很快会返回。
Java 2 SDK Release 1.2以后引入了一些新的api来管理local ref  : EnsureLocalCapacity  NewLocalRef, PushLocalFrame, PopLocalFrame.

JNI规范规定:虚拟机自动保证每个native方法能够创建至少16个local ref ,经验告诉我们,对于一些交互不是很复杂的native调用,这么大的空间足够用了,如果不够用,你可以调用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 */
}
跟之前的版本比,这个版本可能会消耗更多的内存,即使我们DeleteLocalRef。

我们还可以使用Push/PopLocalFrame创建嵌套作用域的local ref ,例如,我们可以重写上面的例子:


#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创建一个新的作用域,在这个作用域你可以使用最多N_REFS个local ref,在PopLocalFrame调用后,这个作用域的local ref会被释放。使用Push/PopLocalFrame的好处是,你可以批量管理。NewLocalRef函数在我们写utils函数的时候比较有用,后面详细介绍。

对于局部引用 local ref 保持良好的习惯,不适用就释放就可以了,文档就是这个意思。如果你的习惯良好,在使用Push/PopLocalFrame的时候就会降低因内存不够退出的可能性。


释放global ref:调用DeleteGlobalRef  DeleteWeakGlobalRef。对于weak global ref需要说明:不使用尽量手动释放,因为,如果你不调用delete手动释放,虽然垃圾回收会把weak global ref指向的对象回收掉,但是,引用本身占的内存并不会被释放。





引用管理的原则:目标是避免不必要的内存使用,和不必要的内存不释放。这是核心。

有两种类型的native代码,一类是直接实现native方法的代码,还有一类是工具代码utils,对于前者,只需要注意一下方法是否返回,多长时间返回,有没有循环,循环多长时间,创建多少local ref,避免不必要的local ref创建。native方法返回前,还有16个local ref没释放是可以接受的。native方法不要创建过多global ref 或global weak ref , 因为这些东西在方法返回前不会被释放,对于global weak ref 至少是释放不干净的。实现utils方法的时候,你必须保证每一个local ref都被手动释放,千万不要漏掉,因为utils是经常被调用的,重复调用的,并且在哪调用的我们是不知道的。当调用utils方法,如果方法返回原生类型数据,我们必须保证,这个调用没有产生额外的local, global, or weak global references , 当方法返回引用类型数据,除了返回的引用类型数据,任何其他的引用local, global, or weak global references 都应该被释放。

这些utils方法可以利用 global or weak global references缓存一些数据,因为只有第一次调用会创建这些对象,因此不会造成内存泄漏。如果utils返回引用,确保这个方法只返回一种类型的引用,不要这种情况下调用返回一个local ref ,那种情况下又返回一个global ref,这样创建者就能够维护好返回的引用。例如GetInfoString是一个utils方法,且被多次调用:

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 SDK release 1.2后,NewLocalRef能够保证返回的总是local ref,下面的例子对MyNewString函数做了一点修改,一个经常使用的字符串"CommonString"被缓存成globa ref

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);//返回缓存的字符串,注意作为local ref返回
    }
    ... /* create the string as a local reference and store in result as a local reference */
    return result;
}

正常的话,会返回一个local ref,作为调用者,得到的永远是local ref, 这个例子也告诉我们,我们可以创建一个global ref的local ref


Push/PopLocalFrame管理local ref实在是太方便了 , native方法开始执行的时候调用PushLocalFrame ,方法返回前调用PopLocalFrame,就能保证之间使用的local ref都能被释放,方便吧!这个方法太好了,文档强烈推荐我们使用。

看一个代码片段来理解一下:

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;
}

看到了吧,在每个return之前都有PushLocalFrame, 也就是说,PushLocalFrame PopLocalFrame是成对调用的。你调用了PushLocalFrame就必须调用PopLocalFrame,否则发生什么谁也无法保证,也许虚拟机会挂掉。

上面的例子同时也表明了为什么有时候需要传PopLocalFrame的第二个参数,result局部引用最开始是在PushLocalFrame创建的帧中创建。PopLocalFrame在最顶部的帧返回之前将第二个参数 result (存在前一个帧当中)转成一个local ref 。 可以这么理解:push1         push2       pop2     pop1, 一个local reference要想在两个frame中都用,那么在PopLocalFrame的第二个参数传入那个需要继续使用的local ref。也就是说除了第二个参数,其余的local ref 全部释放,这样我们就可以return result了。


















评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值