Android JNI开发系列(十二) JNI局部引用、全局引用和弱全局引用

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

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

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

    jclass str = (*env)->FindClass(env, "java/lang/String");
    jcharArray charArray = (*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);
    }

局部引用

  • 建立局部引用测试

    在函数中建立。会阻止GC回收所引用的对象。好比我调用一个NewObject接口建立一个新的对象实例并返回一个对这个对象的局部引用。局部引用只有在建立它的本地方法返回前有效,本地方法返回到Java以后,若是Java层没有对返回的局部引用使用的话,局部引用就会被JVM释放掉。咱们可能在函数中将局部引用存储在静态变量中缓存起来,供下次调用时使用。这种方式是不可取的,由于函数返回后局部引用颇有可能会立刻释放掉,静态变量中存储了就是一个被释放后的内存地址,成为野指针,这种是不能经过判NULL来检测的。附代码:this

    #include <jni.h>
    
    /*错误的局部引用*/
    JNIEXPORT jstring JNICALL Java_org_professor_jni_bean_Person_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 (NULL == cls_string) {
            cls_string = (*env)->FindClass(env, "java/lang/String");
            if (NULL == cls_string) {
                return NULL;
            }
        }
        // 缓存String的构造方法ID
        if (NULL == cid_string) {
            cid_string = (*env)->GetMethodID(env, cls_string, "<init>", "([C)V");
            if (NULL == cid_string) {
                return NULL;
            }
        }
    
        //省略额外的代码.......
        elemArray = (*env)->NewCharArray(env, len);
        // ....
        j_str = (*env)->NewObject(env, cls_string, cid_string, elemArray);
        // 释放局部引用
        (*env)->DeleteLocalRef(env, elemArray);
        return j_str;
    }
    • 分析.net

      上面代码中,略去了无关代码,假设当咱们调用了该方法线程

      JNIEXPORT jstring JNICALL
      JAVA org_professor_jni_MainActivity_callNewString(JNIEnv *env, jobject obj){
          char *c_str = ...;
          jcharArray char_arr = ...;
      
          //...
          return Java_org_professor_jni_bean_Person_newString(env,obj,char_arr,c_str);  
      }

      该方法返回后,JVM会释放在这个方法执行期间建立的全部局部引用,也包含对String的Class引用cls_string。当再次调用Java_org_professor_jni_bean_Person_newString时候,Java_org_professor_jni_bean_Person_newString所指向的内存空间已经被释放,成为了一个野指针,会因非法内存访问形成Crash。

  • 释放局部引用

    释放一个局部引用有两种方式,

    • 本地方法执行完自动释放
    • 手动调用DeleteLocalRef释放

    理所固然,JVM会在函数返回后会被自动释放,为啥还要手动去释放?大部分状况下咱们实现一个Native方法时没必要担忧局部引用的释放问题,函数在被调用完成后,JVM会自动释放函数中所建立的全部局部引用。尽管如此,为避免内存泄漏,某些状况下,咱们应该手动释放局部引用。

    • JNI会将建立的局部引用都存储在一个局部引用表中,若是这个表超过了最大容量限制,就会形成局部引用表溢出,使程序崩溃。经测试,Android上的JNI局部引用表最大数量是512个。当咱们在实现一个本地方法时,可能须要建立大量的局部引用,若是没有及时释放,就有可能致使JNI局部引用表的溢出,因此,在不须要局部引用时就当即调用DeleteLocalRef手动删除。好比,在下面的代码中,本地代码遍历一个特别大的字符串数组,每遍历一个元素,都会建立一个局部引用,当对使用完这个元素的局部引用时,就应该立刻手动释放它。

      for (i = 0; i < len; i++) {
          jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
          ... /* 使用jstr */
           (*env)->DeleteLocalRef(env, jstr); // 使用完成以后立刻释放
      }
    • 在编写JNI工具函数时,工具函数在程序当中是公用的,被谁调用你是不知道的。上面newString这个函数演示了怎么样在工具函数中使用完局部引用后,调用DeleteLocalRef删除。不这样作的话,每次调用newString以后,都会遗留两个引用占用空间(elemArray和cls_string,cls_string不用static缓存的状况下)。

    • 若是你的本地函数不会返回。好比一个接收消息的函数,里面有一个死循环,用于等待别人发送消息过来while(true) { if (有新的消息) { 处理之。。。。} else { 等待新的消息。。。}}。若是在消息循环当中建立的引用你不显示删除,很快将会形成JVM局部引用表溢出。

    • 局部引用会阻止所引用的对象被GC回收。好比你写的一个本地函数中刚开始须要访问一个大对象,所以一开始就建立了一个对这个对象的引用,但在函数返回前会有一个大量的很是复杂的计算过程,而在这个计算过程中是不须要前面建立的那个大对象的引用的。可是,在计算的过程中,若是这个大对象的引用尚未被释放的话,会阻止GC回收这个对象,内存一直占用者,形成资源的浪费。因此这种状况下,在进行复杂计算以前就应该把引用给释放了,以避免没必要要的资源浪费。

      /* 假如这是一个本地方法实现 */
      JNIEXPORT void JNICALL Java_pkg_Cls_func(JNIEnv *env, jobject this)
      {
         lref = ...              /* lref引用的是一个大的Java对象 */
         ...                     /* 在这里已经处理完业务逻辑后,这个对象已经使用完了 */
         (*env)->DeleteLocalRef(env, lref); /* 及时删除这个对这个大对象的引用,GC就能够对它回收,并释放相应的资源*/
         lengthyComputation();   /* 在里有个比较耗时的计算过程 */
         return;                 /* 计算完成以后,函数返回以前全部引用都已经释放 */
      }
  • 管理局部引用

    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);  // 销毁局部引用栈前返回指定的引用
    }

注意:局部引用不能跨线程使用,只在建立它的线程有效。不要试图在一个线程中建立局部引用并存储到全局引用中,而后在另一个线程中使用。

全局引用

全局引用能够跨方法、跨线程使用,直到它被手动释放才会失效。同局部引用同样,也会阻止它所引用的对象被GC回收。与局部引用建立方式不一样的是,只能经过NewGlobalRef函数建立。下面这个版本的newString演示怎么样使用一个全局引用:

JNIEXPORT jstring JNICALL Java_org_professor_jni_bean_Person_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 (local_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;
}

当咱们的本地代码再也不须要一个全局引用时,应该立刻调用DeleteGlobalRef来释放它。若是不手动调用这个函数,即便这个对象已经没用了,JVM也不会回收这个全局引用所指向的对象。

弱全局引用

弱全局引用使用NewGlobalWeakRef建立,使用``DeleteGlobalWeakRef```释放。与全局引用相似,弱引用能够跨方法、线程使用。但与全局引用很重要不一样的一点是,弱引用不会阻止GC回收它引用的对象。在newString这个函数中,咱们也可使用弱引用来存储String的Class引用,由于java.lang.String这个类是系统类,永远不会被GC回收。当本地代码中缓存的引用不必定要阻止GC回收它所指向的对象时,弱引用就是一个最好的选择。假设,一个本地方法org.professor.jni.MainActivity.testWeakRef须要缓存一个指向类org.professor.jni.bean.Student的引用,若是在弱引用中缓存的话,仍然容许类org.professor.jni.bean.Student这个类被unload,由于弱引用不会阻止GC回收所引用的对象。请看下面的代码段:

JNIEXPORT void JNICALL
Java_org_professor_jni_MainActivity_testWeakRef(JNIEnv *env, jobject self) {
    static jclass clazz = NULL;
    if (clazz == NULL) {
        jclass clazzLocal = (*env)->FindClass(env, "org/professor/jni/bean/Student");
        if (NULL == clazzLocal) {
            return;
        }
        clazz = (*env)->NewWeakGlobalRef(env, clazzLocal);
        if (NULL == clazz) {
            return; /* 内存溢出 */
        }
    }
    //省略代码... 
    /* 使用Student的引用 */
}

咱们假设当前类和Student有相同的生命周期(例如,他们可能被相同的类加载器加载),由于弱引用的存在,咱们没必要担忧当前类和它所在的本地代码在被使用时,Student这个类出现先被unload,后来又会preload的状况。固然,若是真的发生这种状况时(当前类和Student此时的生命周期不一样),咱们在使用弱引用时,必须先检查缓存过的弱引用是指向活动的类对象,仍是指向一个已经被GC给unload的类对象。下面立刻告诉你怎样检查弱引用是否活动,即引用的比较。

引用比较

给定两个引用(不论是全局、局部仍是弱全局引用),咱们只须要调用IsSameObject来判断它们两个是否指向相同的对象。例如:(*env)->IsSameObject(env, obj1, obj2),若是obj1和obj2指向相同的对象,则返回JNI_TRUE(或者1),不然返回JNI_FALSE(或者0)。有一个特殊的引用须要注意:NULL,JNI中的NULL引用指向JVM中的null对象。若是obj是一个局部或全局引用,使用(*env)->IsSameObject(env, obj, NULL) 或者 obj == NULL 来判断obj是否指向一个null对象便可。但须要注意的是,IsSameObject用于弱全局引用与NULL比较时,返回值的意义是不一样于局部引用和全局引用的:

jobject local_obj_ref = (*env)->NewObject(env, xxx_cls,xxx_mid);
jobject g_obj_ref = (*env)->NewWeakGlobalRef(env, local_ref);
// ... 业务逻辑处理
jboolean isEqual = (*env)->IsSameObject(env, g_obj_ref, NULL);

在上面的IsSameObject调用中,若是g_obj_ref指向的引用已经被回收,会返回JNI_TRUEg_obj_ref仍然指向一个活动对象,会返回JNI_FALSE

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

引用规则

前面对三种引用已作了一个全面的介绍,下面来总结一下引用的管理规则和使用时的一些注意事项,使用好引用的目的就是为了减小内存使用和对象被引用保持而不能释放,形成内存浪费。因此在开发当中要特别当心!

  • 直接实现Java层声明的native函数的本地代码 当编写这类本地代码时,要小心不要形成全局引用和弱引用的累加,由于本地方法执行完毕后,这两种引用不会被自动释放。

  • 被用在任何环境下的工具函数。例如:方法调用、属性访问和异常处理的工具函数等。

    编写工具函数的本地代码时,要小心不要在函数的调用轨迹上遗漏任何的局部引用,由于工具函数被调用的场合和次数是不肯定的,一量被大量调用,就颇有可能形成内存溢出。因此在编写工具函数时,请遵照下面的规则:

    • 一个返回值为基本类型的工具函数被调用时,它决不能形成局部、全局、弱全局引用被回收的累加

    • 当一个返回值为引用类型的工具函数被调用时,它除了返回的引用之外,它决不能形成其它局部、全局、弱引用的累加

    对于工具函数来讲,为了使用缓存技术而建立一些全局引用或者弱全局引用是正常的。若是一个工具函数返回的是一个引用,咱们应该写好注释详细说明返回引用的类型,以便于使用者更好的管理它们。下面的代码中,频繁地调用工具函数GetInfoString,咱们须要知道GetInfoString返回引用的类型是什么,以便于每次使用完成后调用相应的JNI函数来释放掉它。

    while (JNI_TRUE) {
        jstring infoString = GetInfoString(info);
        ... /* 处理infoString */
        ??? /* 使用完成以后,调用DeleteLocalRef、DeleteGlobalRef、DeleteWeakGlobalRef哪个函数来释放这个引用呢?*/
    }

    函数NewLocalRef有时被用来确保一个工具函数返回一个局部引用。咱们改造一下newString这个函数,演示一下这个函数的用法。下面的newString是把一个被频繁调用的字符串“CommonString”缓存在了全局引用里:

    JNIEXPORT jstring JNICALL
    Java_org_professor_jni_bean_Person_WeakRef(JNIEnv *env, jobject self, jcharArray j_char_arr,
                                           jint len) {
         static jstring result;
        /* 使用wstrncmp函数比较两个Unicode字符串 */
        //省略部分代码
        if (strncmp("CommonString", char_arr, len) == 0) {
            /* 将"CommonString"这个字符串缓存到全局引用中 */
            static jstring cachedString = NULL;
            if (cachedString == NULL) {
                /* 先建立"CommonString"这个字符串 */
                jstring cachedStringLocal = /*...*/;
                /* 而后将这个字符串缓存到全局引用中 */
                cachedString = (*env)->NewGlobalRef(env, cachedStringLocal);
            }
            // 基于全局引用建立一个局引用返回,也一样会阻止GC回收所引用的这个对象,由于它们指向的是同一个对象
            return (*env)->NewLocalRef(env, cachedString);
        }
        //...
        return result;
    }

    在管理局部引用的生命周期中,Push/PopLocalFrame是很是方便且安全的。咱们能够在本地函数的入口处调用PushLocalFrame,而后在出口处调用PopLocalFrame,这样的话,在函数内任何位置建立的局部引用都会被释放。并且,这两个函数是很是高效的,强烈建议使用它们。须要注意的是,若是在函数的入口处调用了PushLocalFrame,记住要在函数全部出口(有return语句出现的地方)都要调用PopLocalFrame。在下面的代码中,对PushLocalFrame的调用只有一次,但调用PopLocalFrame确有屡次,固然你也可使用goto语句来统一处理。

    jobject fun(JNIEnv *env, ...) {
        jobject result;
        if ((*env)->PushLocalFrame(env, 10) < 0) {
            /* 调用PushLocalFrame获取10个局部引用失败,不须要调用PopLocalFrame */
            return NULL;
        }
        //...
        result = ...; // 建立局部引用result
        if (...)
        {
            /* 返回前先弹出栈顶的frame */
            result = (*env)->PopLocalFrame(env, result);
            return result;
        }
        ...
        result = (*env)->PopLocalFrame(env, result);
        /* 正常返回 */
        return result;
    }

    上面的代码一样演示了函数PopLocalFrame的第二个参数的用法,局部引用result一开始在PushLocalFrame建立在当前frame里面,而把result传入PopLocalFrame中时,PopLocalFrame在弹出当前的frame前,会由result生成一个新的局部引用,再将这个新生成的局部引用存储在上一个frame当中。

相关:

相关文章
相关标签/搜索