JNI&NDK开发最佳实践(八):JNI局部引用、全局引用和弱全局引用

三种引用的简介及区别

局部引用

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

jclass cls_string = (*env)->FindClass(env, "java/lang/String");
jcharArray charArr = (*env)->NewCharArray(env, len);
jstring str_obj = (*env)->NewObject(env, cls_string, cid_string, elemArray);
jstring str_obj_local_ref = (*env)->NewLocalRef(env,str_obj);   // 通过NewLocalRef函数创建
...

全局引用

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

static jclass g_cls_string;
void TestFunc(JNIEnv* env, jobject obj) {
    jclass cls_string = (*env)->FindClass(env, "java/lang/String");
    g_cls_string = (*env)->NewGlobalRef(env,cls_string);
}

弱全局引用

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

static jclass g_cls_string;
void TestFunc(JNIEnv* env, jobject obj) {
    jclass cls_string = (*env)->FindClass(env, "java/lang/String");
    g_cls_string = (*env)->NewWeakGlobalRef(env,cls_string);
}

引用管理

引用缓存

1. 为什么要缓存引用?

当我们在本地代码中要访问Java对象的字段或调用它们的方法时,本机代码必须调用FindClass()、GetFieldID()、GetStaticFieldID、GetMethodID() 和 GetStaticMethodID()。对于 GetFieldID()、GetStaticFieldID、GetMethodID() 和 GetStaticMethodID(),为特定类返回的 ID 不会在 JVM 进程的生存期内发生变化。但是,获取字段或方法的调用有时会需要在 JVM 中完成大量工作,因为字段和方法可能是从超类中继承而来的,这会让 JVM 向上遍历类层次结构来找到它们。由于 ID 对于特定类是相同的,因此只需要查找一次,然后便可重复使用。同样,查找类对象的开销也很大,因此也应该缓存它们。

2. 缓存引用的两种方式

2.1 使用时缓存

// AccessCache.c
#include "com_study_jnilearn_AccessCache.h"

JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_accessField
  (JNIEnv *env, jobject obj)
{
    // 第一次访问时将字段存到内存数据区,直到程序结束才会释放,可以起到缓存的作用
    static jfieldID fid_str = NULL;
    jclass cls_AccessCache;
    jstring j_str;
    const char *c_str;
    cls_AccessCache = (*env)->GetObjectClass(env, obj); // 获取该对象的Class引用
    if (cls_AccessCache == NULL) {
        return;
    }

    // 先判断字段ID之前是否已经缓存过,如果已经缓存过则不进行查找
    if (fid_str == NULL) {
        fid_str = (*env)->GetFieldID(env,cls_AccessCache,"str","Ljava/lang/String;");

        // 再次判断是否找到该类的str字段
        if (fid_str == NULL) {
            return;
        }
    }

    j_str = (*env)->GetObjectField(env, obj, fid_str);  // 获取字段的值
    c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
    if (c_str == NULL) {
        return; // 内存不够
    }
    printf("In C:\n str = \"%s\"\n", c_str);
    (*env)->ReleaseStringUTFChars(env, j_str, c_str);   // 释放从从JVM新分配字符串的内存空间

    // 修改字段的值
    j_str = (*env)->NewStringUTF(env, "12345");
    if (j_str == NULL) {
        return;
    }
    (*env)->SetObjectField(env, obj, fid_str, j_str);

    // 释放本地引用
    (*env)->DeleteLocalRef(env,cls_AccessCache);
    (*env)->DeleteLocalRef(env,j_str);
}

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_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, "<init>", "([C)V");
        if (cid_string == NULL) {
            return NULL;
        }
    }

    printf("In C array Len: %d\n", len);
    // 创建一个字符数组
    elemArray = (*env)->NewCharArray(env, len);
    if (elemArray == NULL) {
        return NULL;
    }

    // 获取数组的指针引用,注意:不能直接将jcharArray作为SetCharArrayRegion函数最后一个参数
    chars = (*env)->GetCharArrayElements(env, j_char_arr,NULL);
    if (chars == NULL) {
        return NULL;
    }
    // 将Java字符数组中的内容复制指定长度到新的字符数组中
    (*env)->SetCharArrayRegion(env, elemArray, 0, len, chars);

    // 调用String对象的构造方法,创建一个指定字符数组为内容的String对象
    j_str = (*env)->NewObject(env, cls_string, cid_string, elemArray);

    // 释放本地引用
    (*env)->DeleteLocalRef(env, elemArray);

    return j_str;
}

关键在于利用了C中static关键字在第一次访问时将字段存到内存数据区,直到程序结束才会释放,可以起到缓存的作用。
注意:cls_string是一个局部引用,与方法和字段ID不一样,局部引用在函数结束后会被VM自动释放掉,这时cls_string成为了一个野针对(指向的内存空间已被释放,但变量的值仍然是被释放后的内存地址,不为NULL),当下次再调用Java_com_xxxx_newString这个函数的时候,会试图访问一个无效的局部引用,从而导致非法的内存访问造成程序崩溃。所以在函数内用static缓存局部引用这种方式是错误的。

2.2 类静态初始化缓存

package com.study.jnilearn;

public class AccessCache {

    public static native void initIDs(); 

    public native void nativeMethod();
    public void callback() {
        System.out.println("AccessCache.callback invoked!");
    }

    public static void main(String[] args) {
        AccessCache accessCache = new AccessCache();
        accessCache.nativeMethod();
    }

    static {
        System.loadLibrary("AccessCache");
        initIDs();
    }
}
// AccessCache.c

#include "com_study_jnilearn_AccessCache.h"

jmethodID MID_AccessCache_callback;

JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_initIDs
(JNIEnv *env, jclass cls)
{
    printf("initIDs called!!!\n");
    MID_AccessCache_callback = (*env)->GetMethodID(env,cls,"callback","()V");
}

JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_nativeMethod
(JNIEnv *env, jobject obj)
{
    printf("In C Java_com_study_jnilearn_AccessCache_nativeMethod called!!!\n");
    (*env)->CallVoidMethod(env, obj, MID_AccessCache_callback);
}

JVM加载AccessCache.class到内存当中之后,会调用该类的静态初始化代码块,即static代码块,先调用System.loadLibrary加载动态库到JVM中,紧接着调用native方法initIDs,会调用用到本地函数Java_com_study_jnilearn_AccessCache_initIDs,在该函数中获取需要缓存的ID,然后存入全局变量当中。下次需要用到这些ID的时候,直接使用全局变量当中的即可,如18行当中调用Java的callback函数。

3. static缓存引用的误区

如果用局部引用+static去缓存引用,局部引用在函数结束后会被VM自动释放掉,这时cls_string成为了一个野针对(指向的内存空间已被释放,但变量的值仍然是被释放后的内存地址,不为NULL),当下次再调用Java_com_xxxx_newString这个函数的时候,会试图访问一个无效的局部引用,从而导致非法的内存访问造成程序崩溃。所以在函数内用static缓存局部引用这种方式是错误的,应该使用全局引用。

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString
(JNIEnv *env, jobject obj, jcharArray j_char_arr, jint len)
{
    // ...
    jstring jstr = NULL;
    static jclass cls_string = NULL;
    if (cls_string == NULL) {
        jclass local_cls_string = (*env)->FindClass(env, "java/lang/String");
        if (cls_string == NULL) {
            return NULL;
        }

        // 将java.lang.String类的Class引用缓存到全局引用当中
        cls_string = (*env)->NewGlobalRef(env, local_cls_string);

        // 删除局部引用
        (*env)->DeleteLocalRef(env, local_cls_string);

        // 再次验证全局引用是否创建成功
        if (cls_string == NULL) {
            return NULL;
        }
    }

    // ....
    return jstr;
}

局部引用表溢出

在Android中局部引用表默认最大容量是512个。这是虚拟机实现的,在程序中应该没办法修改这个数量。所以在一个本地方法中,如果使用了大量的局部引用而没有调用env->DeleteLocalRef及时释放的话,随时都有可能造成程序崩溃的现象。

// 给数组中每个元素赋值
for (i = 0; i < count; ++i) {
    memset(buff, 0, sizeof(buff));   // 初始一下缓冲区
    sprintf(buff, c_str_sample,i);
    jstring newStr = (*env)->NewStringUTF(env, buff);
    (*env)->SetObjectArrayElement(env, str_array, i, newStr);
    (*env)->DeleteLocalRef(env,newStr);   // Warning: 这里如果不手动释放局部引用,很有可能造成局部引用表溢出
}

更多JNI&NDK系列文章,参见:
JNI&NDK开发最佳实践(一):开篇
JNI&NDK开发最佳实践(二):CMake实现调用已有C/C++文件中的本地方法
JNI&NDK开发最佳实践(三):CMake实现调用已有so库中的本地方法
JNI&NDK开发最佳实践(四):JNI数据类型及与Java数据类型的映射关系
JNI&NDK开发最佳实践(五):本地方法的静态注册与动态注册
JNI&NDK开发最佳实践(六):JNI实现本地方法时的数据类型转换
JNI&NDK开发最佳实践(七):JNI之本地方法与java互调
JNI&NDK开发最佳实践(八):JNI局部引用、全局引用和弱全局引用
JNI&NDK开发最佳实践(九):调试篇

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349