JNI笔记 : 在JNI中使用引用

1 概述

在Java中,通常分为四种引用类型,分别是:强引用、软引用、弱引用以及虚引用。对于一个Java对象来说,当被强引用所引用时,只要该对象可达,就不会被GC回收;当被软引用所引用时,当内存不足时才有可能会被回收;当被弱引用所引用时,该对象随时都可能被GC回收;而当被虚引用所引用时,可以当做没有引用一样,一般用来判断一个对象是否被回收了。

由此可知,引用类型是比较关键的,它决定了一个Java对象如何被GC管理,所占用的内存是否能够被回收,以及何时能够回收。因此,合理的使用引用类型,可以使GC对内存更好的管理,让程序有更好的性能。对于JNI编程,我们也需要合理的使用引用,滥用和乱用引用可能会导致意想不到的错误,而这种错误在JNI中会尤其严重。

在利于JNI进行编程时,对Java对象和数组类型(诸如jobject, jclass, jstring,andjarray)的处理,通常只能借助相关JNI函数来完成,因为JNI的实现只是将它们作为一个不透明的引用暴露给开发者,也就是说实例和数组的内部结构被隐藏了,其内部构成因此不得而知,我们不能直接对其内部结构进行操作,需要通过预先定义的函数来操作该引用来达到目的。

在本地代码中,通常通过JNI提供的相关函数(定义在JNIE结构中),所分配的内存是受JVM的管理的,只要遵循JNI规范来进行编程即可,但是对于通过malloc、new等分配的内存则需要遵循相关语言的特性,自己来管理内存的分配与释放(使用对应的free、delete来释放内存)。

在JNI方法中中,Java的那一套引用机制也是失效的(比如通过赋值来增加对Java对象的引用是行不通的),如果需要告知JVM哪些对象不能被GC回收,则需要借助JNI定义的引用类型来做特殊处理,在JNI中有如下三种引用类型可供使用:

  • 局部引用( local references)
  • 全局引用(global references)
  • 弱全局引用(weak global references)

上面三种JNI引用的作用不同,作用域也不尽一样,也有着不同的生命周期:

  • 对于局部引用,在本地方法被调用时创建,在方法返回时(return),该引用将会自动被释放,因此在方法返回之后再使用该引用是不合法的。并不是任意引用都可以使用在所有上下文环境。但在其有效时,将一直阻止所引用的对象被GC回收。
  • 对于全局引用和弱全局引用,它们可以在多个方法中使用,在手动释放之前一直有效。但是弱全局引用不会阻止GC回收它所引用的对象,因此使用此引用前判空是必要的。

2 引用详解

2.1 局部引用

  • 绝大多数JNI 方法返回的引用是局部引用,它保证了在引用有效的情况下,所引用的对象不会被GC回收。同时也说明,当局部引用不再使用时,仍然引用了对象而不得回收,造成内存消耗。
  • 局部引用的作用域或生命周期始于创建它的本地方法,终于本地方法返回。因此,不要尝试使用静态变量来缓存局部引用以达到复用,因为缓存的引用将在本地方法返回时变的无效,因而下次继续使用缓存的引用来操作时,将会触发内存崩溃。
  • 通常在局部引用不再使用时,可以显示使用DeleteLocalRef来提前释放它所指向的对象,以便于GC回收。显示的删除局部引用,是为了让它所指的对象能够及时被GC回收,以便节省内存空间,否则只有等到本地方法返回。
  • 在一个局部引用被销毁前,可能经过了数个本地方法的传递,最终会在某个方法里决定销毁它或最终被自动销毁。因此在不需要再使用该局部引用时最好释放它,因为它所引用的可能是大对象,占用较多的内存,长此以往可能造成内存吃紧。另外,在所传递的本地方法中创建了局部引用而不释放它,会造成局部引用的数量积累,严重情况可能导致JNI局部引用表的溢出,导致程序崩溃。
  • 局部引用是线程相关的,只能在创建它的线程里使用,通过全局变量缓存并使用在其他线程是不合法的。

使用静态变量缓存局部引用的错误使用

JNIEXPORT jstring JNICALL Java_com_pecuyu_jnirefdemo_MainActivity_newStringNative(JNIEnv *env, jobject instance,jstring jmsg) {
...

   // 缓存String的class
   static jclass jStringClass;
   if (jStringClass == NULL) { // wrong,局部引用在函数返回后会被释放
       jStringClass = (*env)->FindClass(env, STRING_PATH);
   }

   // 获取构造方法的method id ,参数为byte[]
   jmethodID jinit = (*env)->GetMethodID(env, jStringClass, "", "([B)V");

   int len = strlen(STRING_PATH);
   jcharArray jchars = (*env)->NewByteArray(env, len);  // 创建一个字节数组
   (*env)->SetByteArrayRegion(env, jchars, 0, len, STRING_PATH); // 填充数据

   return (*env)->NewObject(env, jStringClass, jinit, jchars); // 创建并返回String
}

上面方法的意图是通过 new String(byte[])来创建一个String对象,并返回给调用者。

第一次调用时jStringClass为NULL,不会有问题,对jStringClass进行初始化,然后使用它。但是第二次调用时静态的jStringClass就不是NULL了,但是它所引用的对象在第一次调用结束后就已经被释放了,因此再进行使用时,则会触发错误,导致应用crash了。此处,可以去掉缓存或者使用全局引用来实现缓存。

2.2 全局引用

  • 与局部引用的创建不同,需要使用指定的JNI函数来创建全局引用。通过NewGlobalRef方法来创建全局引用,通过DeleteGlobalRef来释放全局引用。
  • 全局引用一直保持有效,直到被程序员手动释放。
  • 在失效之前,全局引用确保了所引用的对象不会被GC回收。
  • 在失效之前,全局引用可以使用在多次方法调用中,也可以跨方法使用,在多个线程中使用也是合法有效的。
  • 可以使用静态变量来缓存全局引用,可以达到复用目的,节省相关性能开销。需要注意的是,全局引用会一直阻止对象被回收,因此合理的释放全局引用是必要的。

如下是对之前代码缓存的改进:

// 缓存string class
  static jclass jStringClass = NULL;
  if (jStringClass == NULL) {
      jclass jlocalClass = (*env)->FindClass(env, STRING_PATH);
      if (jlocalClass == NULL)  
          return NULL;
      jStringClass = (*env)->NewGlobalRef(env, jlocalClass);  // 创建全局引用
      (*env)->DeleteLocalRef(env, jlocalClass); // 删除局部引用
      LOGE("Ref", "jStringClass is null.just create it");
  } else {
      //__android_log_print(ANDROID_LOG_ERROR,"Ref","jStringClass is non-null");
      LOGE("Ref", "jStringClass is non-null");
  }

使用NewGlobalRef来创建全局引用,并缓存在静态的jStringClass变量中。对于之前使用的局部引用jlocalClass,创建全局引用后它已不再使用,并且它本身也会占用空间,使用DeleteLocalRef来删除它。

2.3 弱全局引用:

  • 使用NewWeakGlobalRef来创建,使用DeleteGlobalWeakRef来释放,使引用无效
  • 类似全局引用,在无效之前,可以跨方法多次使用,也可以在多线程中使用,无效时使用会导致异常
  • 在引用有效的情况下,不保证所引用的对象不被GC回收,因此在使用该引用时,需要做判空处理,使用IsSameObject来检查是否存在

弱全局引用的用法与全局引用类似,不同的是调用的jni函数不同。对之前的示例,我们缓存了jclass类型的jStringClass ,对此我们可能会想,能不能同样对jmethodID类型的jinit进行缓存?先不置可否,看如下修改的代码:

static jmethodID jStringMethodID = NULL;
  if (jStringMethodID == NULL) {
      jmethodID jlocalId = (*env)->GetMethodID(env, jStringClass, "", "([B)V");
      jStringMethodID = (*env)->NewWeakGlobalRef(env,jlocalId); // 不可用于创建全局引用
      (*env)->DeleteLocalRef(env,jlocalId);
  } else{
      if((*env)->IsSameObject(env,jStringMethodID,NULL)==JNI_TRUE) // 是否指向对象被回收?
      {
          LOGE("Ref", "jStringMethodID 指向的对象被回收了");  // ??
          jStringMethodID = (*env)->NewWeakGlobalRef(env,(*env)->GetMethodID(env, jStringClass, "", "([B)V"));
      }
  }

当jStringMethodID为NULL,就进行查找,然后再使用若全局引用进行包装。第二次再调用的时候,就可以直接使用之前缓存好的值。IsSameObject用来判断指向的对象是否为同一个,稍后介绍。

这里我们使用弱全局引用来实现,貌似是OK的,但是运行后就会发现应用crash了。为什么会造成这种情况的发生呢?查看jni.h这个头文件,我们很容易的找到jmethodID的定义:

struct _jmethodID;                      /* opaque structure */
typedef struct _jmethodID* jmethodID;   /* method IDs */

可以发现jmethodID实际上是一个结构体指针类型,指向_jmethodID。而NewWeakGlobalRef(JNIEnv*, jobject)需要传递的参数是jobject,实际上是一个void指针类型,因此不可以对jmethodID类型变量进行JNI的引用操作,因为jmethodID所指向的实体推测应该是一个C风格的结构,而非一个Java对象。对于jfieldID也有同样的道理。

    typedef void*           jobject;

假如我们需要缓存jmethodID、jfieldID,直接使用静态变量来缓存即可。比如如下使用:

    static jmethodID jStringMethodID = NULL;
    if (jStringMethodID == NULL) {
        jStringMethodID = (*env)->GetMethodID(env, jStringClass, "", "([B)V");
    } else {
        LOGE("Ref", "jStringMethodID is non-null");
    }

对于经常会被使用的变量,我们可以在一开始就将它们缓存起来。添加如下native方法:

    public native static void nativeInit();

从名字看,是需要让本地代码进行初始化,为了达到在任何函数之前调用,我们可以在load库之后就调用它,如下:

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
        nativeInit(); // 调用
    }

在此native方法的实现里,我们会进行一些初始化功能,如下示例(仅做示例):

    jmethodID g_string_init_mid;
    jobject g_string_class;

    JNIEXPORT void JNICALL
    Java_com_pecuyu_jnirefdemo_MainActivity_nativeInit(JNIEnv *env, jclass type) {
        // init
        g_string_class = (*env)->NewGlobalRef(env, (*env)->FindClass(env, STRING_PATH));
        g_string_init_mid = (*env)->GetMethodID(env, g_string_class, "", "([B)V");
        if(g_string_class==NULL||g_string_init_mid==NULL)
        {
            exit(-1);  // 退出
        }
    }

上面我们缓存了string的class对象以及构造方法String(byte[])的methodId。之后我们调用其他JNI函数,就可以直接使用这两个变量了。如下示例:

    JNIEXPORT jstring JNICALL Java_com_pecuyu_jnirefdemo_MainActivity_newStringNative(JNIEnv *env, jobject instance,
                                                        jstring jmsg) {
        if (jmsg != NULL) {
            jboolean isCopy; // 传出参数
            const char *msg = (*env)->GetStringUTFChars(env, jmsg, &isCopy);
            if (isCopy) // 是否为拷贝,由函数内部决定
            {
                LOGE("Ref", "copy from the origin jmsg");
            }
            LOGE("Ref", "%s", msg);
            (*env)->ReleaseStringUTFChars(env, jmsg, msg);  // 释放字符串占用内存
        }

        int len = strlen(STRING_PATH);
        jcharArray jchars = (*env)->NewByteArray(env, len);  // 创建一个字节数组
        (*env)->SetByteArrayRegion(env, jchars, 0, len, STRING_PATH);

        return (*env)->NewObject(env, g_string_class, g_string_init_mid, jchars); // 创建并返回String
    }

最后,需要特别注意,全局变量缓存的全局引用,需要在全局引用不再使用时进行释放,否则被其所引用的对象得不到回收,而造成了内存泄露。添加如下方法,用来释放全局引用:

    public native static void delRefs();

    // implementation
    JNIEXPORT void JNICALL
    Java_com_pecuyu_jnirefdemo_MainActivity_delRefs(JNIEnv *env, jclass type) {

        // free global refs
        (*env)->DeleteGlobalRef(env,g_string_class);
        //...
    }

2.4 引用比较

使用IsSameObject比较上面三种引用的任意两个,可以比较是否指向了同一个对象。

  • 函数原型如下:

jboolean (IsSameObject)(JNIEnv, jobject, jobject);

方法返回:

  • JNI_TRUE(or 1) 引用同一个对象
  • JNI_FALSE(or0) 引用非同一个对象

JNI中的NULL指向了JVM的null,与NULL作比较实际上是判断对象引用是否为null,使用如下:

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

对于一个非NULL的弱全局引用,我们可以通过与NULL的比较,来判断它所引用的对象是否还存活,如下代码:

(*env)->IsSameObject(env, wobj, NULL)
  • 返回JNI_TRUE表示弱全局引用wobj指向了null,即它所引用的对象已经被JVM回收
  • 返回 JNI_FALSE 表示它所引用的对象还存活

3 释放引用

三点概要:

内存占用:

每个正在使用的JNI引用,不仅被它所引用的对象会占据内存,它自身也会占一定的内存空间,因此适时释放JNI引用是很必要的。

局部引用数量:

我们需要关注在特定时间下创建的局部引用的数量,因为在程序执行的某个时间点上,所能创建的局部引用的数量是有上限的,即过多的数量可能会导致JNI内部的局部引用表溢出。

内存溢出风险

虽然局部引用可以不经处理,在本地方法返回时最终仍然会自动被虚拟机释放,但是在过多的局部引用的情况下,可能导致很多对象得不到释放(尤其是有大对象被引用时),而这可能导致应用内存耗尽,即可能出现内存溢出的风险。

3.1 释放局部引用

  • 在某个本地方法调用中,创建的局部引用的数量不宜过多,否则可能导致JNI内部的局部引用表溢出,应该适时释放不再使用的局部引用。尽管在本地方法返回后,局部引用会被自动释放,但是在这之前若不主动释放,局部引用所引用的对象会一直存活,可能导致使用的内存在方法调用时居高不下,甚至可能出现OOM的风险。
  • 当一个本地方法不再返回,也就是内部无限循环执行,此时适时释放无用局部引用是必须的。例如,一个本地方法可能进入一个无终止的事件派遣循环。释放在循环中创建的局部引用至关重要(crucial),因此不会让引用数量累增而导致内存泄露
  • 局部引用引用了一个较大的对象。在本地方法调用时,可能会访问java中大对象,局部引用在被释放之前会一直阻止JVM回收这个大对象,即是之后不再使用该对象,造成了内存浪费,因此适时释放该局部引用是合理的选择。
  • 使用DeleteLocalRef 来使用局部引用

下面的例子是在循环中不断创建局部引用,但是不主动释放它们,看看会有什么结果:

JNIEXPORT void JNICALL
Java_com_pecuyu_jnirefdemo_MainActivity_refsOverflow(JNIEnv *env, jobject instance) {

    jstring jstr;
    char buf[60];
    for (int i = 0; i < 1000; ++i) {
        sprintf(buf,"new string %d",i);
        jstr=(*env)->NewStringUTF(env,buf); // 创建一个java string
    }

}

使用for循环来不停的创建string对象,却没有主动释放。要知道,和局部变量不同的是,局部引用的重新赋值,并不会改变已经创建了的局部引用的指向,除非手动删除,否则会一直积累,直到JNI函数完全返回,所有的局部引用才会都被释放。

因此本例的局部引用的数量会一直积累,但因为超过了局部引用表容量的最大值,导致了错误发生,如下输出:

com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427] JNI ERROR (app bug): local reference table overflow (max=512)
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427] local reference table dump:
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427]   Last 10 entries (of 512):
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427]       511: 0x12dae938 java.lang.String "new string : 505" 
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427]       510: 0x12dae900 java.lang.String "new string : 504" 
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427]       509: 0x12dae8c8 java.lang.String "new string : 503" 
                                                                ...
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427]       502: 0x12dae740 java.lang.String "new string : 496" 
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427]   Summary:
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427]       509 of java.lang.String (509 unique instances)
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427]         2 of java.lang.Class (2 unique instances)
/com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427]         1 of java.lang.String[] (3 elements)

从错误可以看出,是因为local reference table overflow ,也就是局部引用表溢出了,最大值是512,而这里曾经的很明显超了。 从下面的摘要Summary中,可以看到具体的使用情况。要想解决问题,也就一行代码,及时的删除局部引用:

... 
for (int i = 0; i < 1000; ++i) {
       sprintf(buf,"new string : %d",i); // 格式化字串
       jstr=(*env)->NewStringUTF(env,buf); // 创建一个java string
       (*env)->DeleteLocalRef(env,jstr);  // 删除局部引用
}
...

下面的一个例子是引用大对象,当不再使用时,就马上释放它,让内存得到释放(来自官方示例):

    /* A native method implementation */
    JNIEXPORT void JNICALL
    Java_pkg_Cls_func(JNIEnv *env, jobject this)
    {
        lref = ... /* 引用一个占大内存的Java对象 */
        ... /* 最后使用引用lref */
        (*env)->DeleteLocalRef(env, lref); // 释放局部引用
        lengthyComputation(); /* 其他一些耗时的操作 */
        return; /* all local refs are freed */
    }

3.2 Java2的新特性

Java2开始提供了一套新的方法来管理局部引用的生命周期,包括 EnsureLocalCapacity, NewLocalRef, PushLocalFrame, 以及 PopLocalFrame.

JNI规范规定,JVM自动保证每个本地方法都至少能创建16个局部引用(实际支持的数量可能大得多)。通常对于和JVM的对象没有复杂交互的本地方法来说,这个数量是足够的,但是如果需要创建额外的局部引用,我们可以调用EnsureLocalCapacity方法来确保一定数量的局部引用的空间是可用的。使用Push/PopLocalFrame来允许程序员创建一个局部引用的嵌套作用域,如下一段官方示例:

    * The number of local references to be created is equal to
    the length of the array. */
    if ((*env)->EnsureLocalCapacity(env, len)) < 0) {  // 确保是否有len个局部引用的空间
    ... /* out of memory */
    }
    #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) {   // push
        ... /* out of memory */
        }
        jstr = (*env)->GetObjectArrayElement(env, arr, i);
        ... /* process jstr */
        (*env)->PopLocalFrame(env, NULL);    //  pop
    }

PushLocalFrame为给定数量的局部引用创建了一个新的作用域.PopLocalFrame销毁了之前创建的作用域,并释放了在此作用域的局部引用(两个函数之间创建的局部引用)。使用这两个函数的好处是显而易见的,它们动态创建了一个嵌套的作用域,可以很方便的管理其中的局部变量的生命周期,而不必管其中创建的任意一个引用,因此在PopLocalFrame被调用后,该作用域被销毁,而其中的所有局部引用将会被释放。

在本地代码中,可能创建超过16个引用,或者在Push/PopLocalFrame之间的作用域里,可能创建了超过之前所能保证的容量,JVM的实现会尽可能为局部引用分配内存,但是不能保证一定有可用内存。当无法分配足够内存时,虚拟机会退出,造成应用崩溃。因此需要及时的释放局部引用,以保证局部变量有足够的内存,避免出错。

接下来看一个Android源码中相关的例子,AndroidRuntime在启动后,调用这个函数是用来注册一些与VM相关的常用JNI函数,将Java层与native层连接起来:

/*
 * Register android native functions with the VM.
 */
/*static*/ int AndroidRuntime::startReg(JNIEnv* env)
{
    ATRACE_NAME("RegisterAndroidNatives");
    /*
     * This hook causes all future threads created in this process to be
     * attached to the JavaVM.  (This needs to go away in favor of JNI
     * Attach calls.)
     */
    androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);

    ALOGV("--- registering native functions ---\n");

    /*
     * Every "register" function calls one or more things that return
     * a local reference (e.g. FindClass).  Because we haven't really
     * started the VM yet, they're all getting stored in the base frame
     * and never released.  Use Push/Pop to manage the storage.
     */
    env->PushLocalFrame(200);  // push 方法

    if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
        env->PopLocalFrame(NULL);   // 注册失败也需要pop
        return -1;
    }
    env->PopLocalFrame(NULL);  // pop方法

    //createJavaThread("fubar", quickTest, (void*) "hello");

    return 0;
}

接下来,通过实例来测试一下Push/PopLocalFrame的作用,下面的测试是循环1000次,每次都在内部循环创建100个string,若不使用Push/PopLocalFrame,则肯定是直接局部引用表溢出了,但当使用Push/PopLocalFrame时,程序表现的很正常。可以知道,每次内循环创建的100个局部引用,在pop方法调用后,就被释放掉了:

JNIEXPORT void JNICALL
Java_com_pecuyu_jnirefdemo_MainActivity_pushPopRefs(JNIEnv *env, jobject instance) {

    jstring jstr;
    char buf[60];
    for (int i = 0; i < 1000; ++i) {

        (*env)->PushLocalFrame(env, 100);  // push

        for (int j = 0; j < 100; ++j) {
            sprintf(buf, "new string : %d", j);
            jstr = (*env)->NewStringUTF(env, buf); // 创建一个java string
        }

        (*env)->PopLocalFrame(env, NULL);  // pop
    }

}

NewLocalRef函数可以用来创建一个局部引用,参数可以是局部引用,全局引用,以及弱全局引用。

Java2支持命令行选项-verbose:jni。当此选项被开启,当局部引用的数量超过预设容量时,JVM将报告此问题。

3.3 释放全局引用

  • 在本地代码中,当一个全局引用不再使用时,调用DeleteGlobalRef函数来释放它,从而被它所引用的对象能够被GC回收。假如没有调用该函数来删除此全局引用(或调用函数失败),那么它所引用的对象将一直不会被GC回收,即使所引用的对象已经不再使用了,这将导致潜在的内存泄露,长此以往则可能导致应用的性能问题甚至崩溃。
  • 在本地代码中,当一个弱全局引用不再使用时,调用DeleteWeakGlobalRef函数来释放它。即使不手动释放此引用(或调用函数失败),GC也可能回收了它所引用的对象,但是它本身所占用的内存却得不到回收。因此在使用该引用时,需要做判空处理(使用IsSameObject)。

释放全局引用的例子如下:

    JNIEXPORT void JNICALL
    Java_com_pecuyu_jnirefdemo_MainActivity_delRefs(JNIEnv *env, jclass type) {

        // free global refs
        (*env)->DeleteGlobalRef(env,g_string_class); // 对于弱全局引用,需要调用DeleteWeakGlobalRef
        //...
    }

4 管理JNI引用的原则

一般有两种风格本地代码需要注意:

  • 直接实现native方法的函数(JNI函数)
  • 使用在任意上下文的工具函数

4.1 直接实现native方法的函数:

  • 1、避免在循环中创建了过多局部引用,而没有及时释放,以免造成局部引用表溢出。在有循环或大量创建局部引用的地方,可以使用Push/PopLocalFrame来管理这些局部引用,不必为创建的引用没有释放而担心。
  • 2、在本地方法返回前,应该尽可能的主动释放不再使用的引用,即使创建的局部引用的数量没有超过局部引用表的大小。
  • 3、避免在不返回的函数中创建了引用,但没有及时释放无用的引用
  • 4、native方法的多次调用,不应该导致全局或弱全局引用的累加,因为在调用返回后它们是不会被自动释放的。

4.2 本地工具函数:

  • 1、在整个函数的任意执行路径上,都不能泄露局部引用(因为工具函数可能会在任意上下文环境被重复调用,因此多余的引用可能会导致内存溢出)
  • 2、如果工具函数返回原始类型,不应该有积累局部、全局以及弱全部引用的副作用
  • 3、如果工具函数返回引用类型,除了返回值引用外,不应该有积累其他局部、全局以及弱全部引用的副作用
  • 4、在工具函数里面是可以创建全局、弱全局引用的,因为它们只会在最开始的一次调用才会创建,其他调用应该确保不会重复创建
  • 5、确保工具函数的返回值类型是一定的,不能一会返回局部引用,一会又返回全局引用,因为调用者需要明确返回的引用类型,以方便使用哪个JNI函数来释放返回的引用

举个栗子:

jstring newStringWithMsg(JNIEnv *env, const char *msg) {
    int len = strlen(msg);
    jcharArray jchars = (*env)->NewByteArray(env, len);  // 创建一个字节数组
    (*env)->SetByteArrayRegion(env, jchars, 0, len, msg);

    jstring jstr = (*env)->NewObject(env, g_string_class, g_string_init_mid, jchars);  // 创建string对象

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

    return jstr;
}

对于上面的工具函数,删除jchars局部引用是必要的,否则当该函数被频繁调用时,可能导致局部引用表的溢出。

4.3 Push/PopLocalFrame管理局部变量的生命周期

jint PushLocalFrame(JNIEnv *env, jint capacity);
jobject PopLocalFrame(JNIEnv *env, jobject result);

  • 1、两个函数成对使用,调用了push方法而没有调用pop方法,可能导致不可预知的错误。换句话说,对于函数返回的任意一条路径上,都必须对应的调用pop方法,不能存在一条执行路径上调用了push但没有调用pop的情况。
  • 2、在两个函数之间创建的局部引用的生命周期被限制在这之间,pop函数调用后,会释放这之间创建的所有引用
  • 3、PushLocalFrame函数返回0表示成功,否则返回负数,并抛出OutOfMemoryError
  • 4、使用PopLocalFrame时,可以传递一个之前使用的局部引用做参数,以保证它不会被释放,通过返回值获取该引用

针对最后一点,来举个栗子:

  ...
  jstring jstr = NULL;
   jstring result = NULL;
   char buf[60];

   if ((*env)->PushLocalFrame(env, 100) < 0)  // push
    {
        return;  // OutOfMemoryError
    }

   for (int j = 0; j < 100; ++j) {
       sprintf(buf, "new string : %d", j);
       jstr = (*env)->NewStringUTF(env, buf); // 创建一个java string
   }

   // 保留最后一条jstr引用,不会被释放,通过返回值返回该引用
   result = (*env)->PopLocalFrame(env, jstr);  // pop

   if (result != NULL) {
       const char *chs = (*env)->GetStringUTFChars(env, result, NULL);
       LOGE("refs", "chs = %s", chs);
       (*env)->DeleteLocalRef(env, result);
   }
   ...

4.4 小结

总结一下各种引用的特点:

- 局部引用 全局引用 弱全局引用
是否跨方法 - 支持 支持
是否跨线程 - 支持 支持
引用释放 native方法返回或调用DeleteLocalRef 调用DeleteGlobalRef 调用DeleteWeakGlobalRef
引用对象回收时机 引用释放后 引用释放后 不阻止对象回收
缓存 - 支持 支持

拓展阅读:JNI笔记 : 数据类型、JNI函数与签名


参考:The Java Native Interface Programmer’s Guide and Specification

你可能感兴趣的:(Java)