JNI 局部引用、全局引用、弱全局引用

在jni规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。区别如下:

  • 局部引用
    通过 NewLocalRef 和各种 JNI 接口创建(FindClass、NewObject、GetObjectClass和NewCharArray等)。会阻止 GC 回收所引用的对象,不在本地函数中跨函数使用,不能跨线程使用。函数返回后局部引用所引用的对象会被JVM 自动释放,或调用 DeleteLocalRef 释放。(*env)->DeleteLocalRef(env,local_ref)

  • 全局引用
    调用 NewGlobalRef 基于局部引用创建,会阻 GC 回收所引用的对象。可以跨方法、跨线程使用。JVM 不会自动释放,必须调用 DeleteGlobalRef 手动释放。(*env)->DeleteGlobalRef(env,g_cls_string)

  • 弱全局引用
    调用 NewWeakGlobalRef 基于局部引用或全局引用创建,不会阻止 GC 回收所引用的对象,可以跨方法、跨线程使用。引用不会自动释放,在 JVM 认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放。或调用DeleteWeakGlobalRef 手动释放。(*env)->DeleteWeakGlobalRef(env,g_cls_string)

1.局部引用

局部引用也称本地引用,通常是在函数中创建并使用。会阻止 GC 回收所引用的对象。比如,调用 NewObject 接口创建一个新的对象实例并返回一个对这个对象的局部引用。局部引用只有在创建它的本地方法返回前有效,本地方法返回到 Java 层之后,如果 Java 层没有对返回的局部引用使用的话,局部引用就会被 JVM 自动释放。你可能会为了提高程序的性能,在函数中将局部引用存储在静态变量中缓存起来,供下次调用时使用。这种方式是错误的,因为函数返回后局部引很可能马上就会被释放掉,静态变量中存储的就是一个被释放后的内存地址,成了一个野针对,下次再使用的时候就会造成非法地址的访问,使程序崩溃。请看下面一个例子,错误的缓存了 String 的 Class 引用。

/*错误的局部引用*/
JNIEXPORT jstring JNICALL Java_com_kun_jni_1callback_MainActivity_newString
        (JNIEnv *env, jobject obj, jcharArray j_char_arr, jint len) {
    jcharArray elemArray;
    jchar *chars = NULL;
    jstring j_str = NULL;
    static jclass cls_string = NULL;
    static jmethodID cid_string = NULL;
    
    // 注意:错误的引用缓存
    if (cls_string == NULL) {
        cls_string = (*env)->FindClass(env, "java/lang/String");
        if (cls_string == NULL) {
            return NULL;
        }
    }
    // 缓存String的构造方法ID
    if (cid_string == NULL) {
        cid_string = (*env)->GetMethodID(env, cls_string, "", "([C)V");
        if (cid_string == NULL) {
            return NULL;
        }
    }

    //省略额外的代码.......
    elemArray = (*env)->NewCharArray(env, len);
    // ....
    j_str = (*env)->NewObject(env, cls_string, cid_string, elemArray);
    // 释放局部引用
    (*env)->DeleteLocalRef(env, elemArray);
    return j_str;
}

下面,我们来分析一下为什么会有这个问题。首先,局部引用是通过 NewLocalRef 和各种 JNI 接口(FindClass、NewObject、GetObjectClass和NewCharArray等)来创建的。如下图,cls_string = (*env)->FindClass(env, "java/lang/String"),FindClass会在局部引用表上创建一个局部引用,并返回这个局部引用的地址,这个局部引用指向java heap上的String class对象。而cls_string持有了这个局部变量的引用,cls_string是个静态局部变量,所以它的生命周期和程序的生命周期一样。第一次给cls_string赋值的时候,没问题,通过cls_string是可以访问这个String的class对象,但是,当方法返回(方法执行完)时,jvm会去回收这个局部引用表,这个时候cls_string所指向的内存被回收了,它就成了一个野指针。当下次在执行这个方法的时候就会报:use of deleted local reference 错误。

JNI 局部引用、全局引用、弱全局引用_第1张图片
局部引用.png

要避免这个错很简单,只要把这个局部引用改为全局引用就好。

cls_string = (jclass) (*env)->NewGlobalRef(env,(*env)->FindClass(env,"java/lang/String"));

局部引用会阻止所引用的对象被 GC 回收。比如你写的一个本地函数中刚开始需要访问一个大对象,因此一开始就创建了一个对这个对象的引用,但在函数返回前会有一个大量的非常复杂的计算过程,而在这个计算过程当中是不需要前面创建的那个大对象的引用的。但是,在计算的过程当中,如果这个大对象的引用还没有被释放的话,会阻止 GC 回收这个对象,内存一直占用者,造成资源的浪费。所以这种情况下,在进行复杂计算之前就应该把引用给释放了,以免不必要的资源浪费。

2.全局引用

全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效。同局部引用一样,也会阻止它所引用的对象被 GC 回收。与局部引用创建方式不同的是,只能通过 NewGlobalRef 函数创建。为了避免内存泄漏,需要在不用的时候调运(*env)->DeleteLocalRef()来释放掉。下面来看一个实例:

extern "C"
JNIEXPORT void JNICALL
Java_com_kun_jni_1callback_MainActivity_callNativeMethod(JNIEnv *env, jobject instance,
                                                         jobject obj) {
    jclass clz = (jclass) env->NewGlobalRef(env->GetObjectClass(instance));

    /* 使用clz*/

    env->DeleteGlobalRef(clz);
}    

每一个 JNI 引用被建立时,除了它所指向的 JVM 中对象的引用需要占用一定的内存空间外,引用本身也会消耗掉一个数量的内存空间。作为一个优秀的程序员,我们应该对程序在一个给定的时间段内使用的引用数量要十分小心。短时间内创建大量而没有被立即回收的引用很可能就会导致内存溢出。     当我们的本地代码不再需要一个全局引用时,应该马上调用 DeleteGlobalRef 来释放它。如果不手动调用这个函数,即使这个对象已经没用了,JVM 也不会回收这个全局引用所指向的对象。

3.弱全局引用

弱全局引用使用 NewGlobalWeakRef 创建,使用 DeleteGlobalWeakRef 释放。下面简称弱引用。与全局引用类似,弱引用可以跨方法、线程使用。但与全局引用很重要不同的一点是,弱引用不会阻止 GC 回收它引用的对象。当本地代码中缓存的引用不一定要阻止 GC 回收它所指向的对象时,弱引用就是一个最好的选择。看下面的代码段:

static const char *kTAG = "native-lib";
#define LOGE(...) \
  ((void)__android_log_print(ANDROID_LOG_ERROR, kTAG, __VA_ARGS__))

extern "C"
JNIEXPORT void JNICALL
Java_com_kun_jni_1callback_MainActivity_weakRefMethod(JNIEnv *env, jobject instance) {

    static jclass clz = NULL;
    if (clz == NULL) {
        jclass clzLocal = env->GetObjectClass(instance);
        if (clzLocal == NULL) {
            return; /* 没有找到这个类 */
        }
        clz = (jclass) env->NewWeakGlobalRef(clzLocal);
        if (clz == NULL) {
            return; /* 内存溢出 */
        }
    }
    /* 使用clz的引用 */
    if (env->IsSameObject(clz, NULL)) {
        LOGE("clz is recycled");
    } else {
        LOGE("clz is not recycled");
    }
}

我们在使用弱引用的时候,必须先判断这个弱引用是指向一个活动的对象,还是一个已经被GC回收来的对象。NULL引用指向jvm中的null对象,可以通过IsSameObject来判断这引用是否被回收。通过env->IsSameObject(clz, NULL)来判断,被回收了则返回 JNI_TRUE(或者 1),否则返回 JNI_FALSE(或者 0)。

当我们的本地代码不再需要一个弱全局引用时,也应该调用 DeleteWeakGlobalRef 来释放它,如果不手动调用这个函数来释放所指向的对象,JVM 仍会回收弱引用所指向的对象,但弱引用本身在引用表中所占的内存永远也不会被回收。

4.管理局部引用

JNI 提供了一系列函数来管理局部引用的生命周期。这些函数包括:EnsureLocalCapacity、NewLocalRef、PushLocalFrame、PopLocalFrame、DeleteLocalRef。JNI 规范指出,任何实现 JNI 规范的 JVM,必须确保每个本地函数至少可以创建 16 个局部引用(可以理解为虚拟机默认支持创建 16 个局部引用)。实际经验表明,这个数量已经满足大多数不需要和 JVM 中内部对象有太多交互的本地方函数。如果需要创建更多的引用,可以通过调用 EnsureLocalCapacity 函数,确保在当前线程中创建指定数量的局部引用,如果创建成功则返回 0,否则创建失败,并抛出 OutOfMemoryError 异常。EnsureLocalCapacity 这个函数是 1.2 以上版本才提供的,为了向下兼容,在编译的时候,如果申请创建的局部引用超过了本地引用的最大容量,在运行时 JVM 会调用 FatalError 函数使程序强制退出。在开发过程当中,可以为 JVM 添加-verbose:jni参数,在编译的时如果发现本地代码在试图申请过多的引用时,会打印警告信息提示我们要注意。在下面的代码中,遍历数组时会获取每个元素的引用,使用完了之后不手动删除,不考虑内存因素的情况下,它可以为这种创建大量的局部引用提供足够的空间。由于没有及时删除局部引用,因此在函数执行期间,会消耗更多的内存。

/*处理函数逻辑时,确保函数能创建len个局部引用*/
if((*env)->EnsureLocalCapacity(env,len) != 0) {
... /*申请len个局部引用的内存空间失败 OutOfMemoryError*/
return;
}
for(i=0; i < len; i++) {
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
// ... 使用jstr字符串
/*这里没有删除在for中临时创建的局部引用*/
}

另外,除了 EnsureLocalCapacity 函数可以扩充指定容量的局部引用数量外,我们也可以利用 Push/PopLocalFrame 函数对创建作用范围层层嵌套的局部引用。例如,我们把上面那段处理字符串数组的代码用 Push/PopLocalFrame 函数对重写。

#define N_REFS ... /*最大局部引用数量*/
for (i = 0; i < len; i++) {
if ((*env)->PushLocalFrame(env, N_REFS) != 0) {
... /*内存溢出*/
}
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* 使用jstr */
(*env)->PopLocalFrame(env, NULL);
}

PushLocalFrame 为当前函数中需要用到的局部引用创建了一个引用堆栈,(如果之前调用 PushLocalFrame 已经创建了 Frame,在当前的本地引用栈中仍然是有效的)每遍历一次调用(*env)->GetObjectArrayElement(env, arr, i);返回一个局部引用时,JVM 会自动将该引用压入当前局部引用栈中。而 PopLocalFrame 负责销毁栈中所有的引用。这样一来,Push/PopLocalFrame 函数对提供了对局部引用生命周期更方便的管理,而不需要时刻关注获取一个引用后,再调用 DeleteLocalRef 来释放引用。在上面的例子中,如果在处理 jstr 的过程当中又创建了局部引用,则 PopLocalFrame 执行时,这些局部引用将全都会被销毁。在调用 PopLocalFrame 销毁当前 frame 中的所有引用前,如果第二个参数 result 不为空,会由 result 生成一个新的局部引用,再把这个新生成的局部引用存储在上一个 frame 中。请看下面的示例。

// 函数原型
jobject (JNICALL *PopLocalFrame)(JNIEnv *env, jobject result);

jstring other_jstr;
for (i = 0; i < len; i++) {
if ((*env)->PushLocalFrame(env, N_REFS) != 0) {
... /*内存溢出*/
}
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* 使用jstr */
if (i == 2) {
other_jstr = jstr;
}
other_jstr = (*env)->PopLocalFrame(env, other_jstr);  // 销毁局部引用栈前返回指定的引用
}

你可能感兴趣的:(JNI 局部引用、全局引用、弱全局引用)