第五章 JNI 中局部引用和全局引用

原文 : http://192.9.162.55/docs/books/jni/html/refs.html


    JNI给出实例和数组类型(如jobject,jclass,jstring,jarray)作为不透明的引用。Native代码从不会直接访问一个不透明的引用的内容。相反,它使用JNI函数访问由一个不透明的参考指向数据结构。只处理不透明的引用,你不用担心依赖于特定的Java虚拟机实现的内部对象布局。但是你的确需要进一步了解JNI中不同类型的引用:

  • JNI 支持三种不透明引用:局部引用( local references),全局引用(global references)和弱全局引用( weak global references)。
  • 局部和全局引用有不同的生命周期,本地应用被自动释放,而全局应用和弱全局引用仅在显式调用释放命令时释放。
  • 一个局部和全局引用保持引用对象不被garbage内存回收。另一方面,弱全局引用是允许引用对象被garbage内存回收的。
  • 并非所有的引用都能在任何上下文中被使用。例如,在创建引用的native函数返回后使用这个引用是不合法的。 

5.1 局部和全局引用

    什么是局部和全局引用,他们有什么差别?下面将用一些例程来说明。

5.1.1 局部和全局引用

    很多 JNI 函数创建局部引用。例如,JNI函数 New-Object 创建一个新的实例并返回这个实例的局部引用。 

    一个局部引用仅在创建它的native函数及该函数调用的函数中有效。在一个native函数执行期间创建的所有局部引用将在该函数返回时被释放。
    你一定不能写一个native函数来保存一个局部引用在静态变量中并期望在以后的函数调用中使用它。例如,以下程序是4.4节中 MyNewString 函数的修改版本,他错误使用了局部引用。

 /* This code is illegal */
 jstring
 MyNewString(JNIEnv *env, jchar *chars, jint len)
 {
     static jclass stringClass = NULL;
     jmethodID cid;
     jcharArray elemArr;
     jstring result;
 
     if (stringClass == NULL) {
         stringClass = (*env)->FindClass(env,
                                         "java/lang/String");
         if (stringClass == NULL) {
             return NULL; /* exception thrown */
         }
     }
     /* 这儿使用缓冲过的 stringClass 是错误的,因为他可能是无效了 */
     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;
 }
    这儿省略了部分代码和我们要讨论的问题无关。用一个静态变量缓存stringClass的目地本来是要消除重复下面的函数调用的开销:
FindClass(env, "java/lang/String");
    这是不正确的做法,因为findClass返回java.lang.String类对象的局部引用。要明白为什么这是一个问题,假设比照调用Native方法的实现MyNewString的:
JNIEXPORT jstring JNICALL
 Java_C_f(JNIEnv *env, jobject this)
 {
     char *c_str = ...;
     ...
     return MyNewString(c_str);
 }
    Native方法C.f 返回后,虚拟机释放了执行Java_C_f 期间创建的所有局部引用。这些被释放的局部引用包含存储在stringClass变量中的对类对象局部引用。以后的MyNewString 将试图使用无效的局部引用,这可能会导致内存数据损坏或系统崩溃。例如,下面的Java代码段连续两次调用C.f 并造成 MyNewString 遭遇到无效的局部引用:
 ...
 ... = C.f(); // The first call is perhaps OK.
 ... = C.f(); // This would use an invalid local reference.
 ...
    有两种方法使一个局部引用无效。如前所述,当native方法返回后,虚拟机自动释放了方法执行期间创建的所有局部引用。另外,程序员可以用像DeleteLocalRef 这样的JNI函数显式管理局部引用的生命周期。
    既然在native方法返回后虚拟机会自动释放期间创建的所有局部引用,那为什么需要显式删除局部引用呢? 一个局部引用直到它无效时才会让garbage收集引用对象。例如, 在MyNewString 中的 DeleteLocalRef 调用允许中间数组对象 elemArr 立即被garbage收集。否则虚拟机将只能在调用MyNewString 的native方法(如C.f)返回后才能释放他。 

    一个局部引用在被销毁前可能在多个native方法件被传递。例如,MyNewString 返回NewObject创建的字符串引用,然后它将被传给 MyNewString 的调用者以决定是否释放MyNewString 返回的局部引用。 在Java_C_f 例子中,C.f 返回 MyNewString 返回的结果。虚拟机从 Java_C_f 函数收到局部引用之后,它传递下层的字符串对象给 C.f 的调用者,并且随后销毁原本由JNI函数NewObject 创建的局部引用。

    局部引用也仅在创建它们的线程中有效。在一个线程中创建的局部引用不能为另一个线程所用。在native 方法中存储一个局部引用到全局变量并期望在另一个线程中使用它是错误的

5.1.2 全局引用

    你能在一个native方法的多个调用间使用一个全局引用。一个全局引用也能在多线程件被使用,直到被程序员显式释放。类似局部引用,一个全局引用在被释放前保证引用对象不被garbage回收。
    和局部引用不同的是,没有那么多函数能够创建全局引用。能创建全局引用的函数只有 NewGlobalRef。以下 MyNewString 版本说明了如何使用一个全局引用。我们要注意这段代码和上段错误代码的不同。
 /* 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");
         if (localRefCls == NULL) {
             return NULL; /* exception thrown */
         }
         /* Create a global reference */
         stringClass = (*env)->NewGlobalRef(env, localRefCls);
 
         /* 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 */
         }
     }
     ...
 }
    修改后的版本将 FindClass返回的局部引用传递给 NewGlobalRef, NewGlobalRef 创建一个 java.lang.String 类对象的全局引用。我们在删除localRefCls 后检查 NewGlobalRef 是否成功创建stringClass,因为无论检查结果如何,局部引用 localRefCls 都需要被删除。 

5.1.3 弱全局引用

    弱全局引用是 Java 2 SDK r1.2中新增的。弱全局引用用 NewGlobalWeakRef 创建,用 DeleteGlobalWeakRef 释放。类似全局引用,弱全局引用在native方法和不同线程间保持有效。与之不同的是,弱全局引用不会阻止底层对象被garbage 收集。 
    MyNewString 例程已经展示如何缓冲一个全局引用到java.lang.String 类。MyNewString 例子也可以换用一个弱全局引用来存储缓存的 java.lang.String 类。这儿,我们用全局引用还是弱全局引用都是可以的。因为 java.lang.String 是一个系统类,它永远不会被garbage收集。
    弱全局引用更有用的地方是当一个由native代码缓存的引用必须不阻碍底层对象被garbage收集时。,例如,假设一个native方法 mypkg.MyCls.f 需要缓存一个引用到类mypkg.MyCls2。 在弱全局引用中缓存这个类允许 mypkg.MyCls2 类还要被卸载。 
 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 */
 }
    我们假设 MyCls 和 MyCls2 有相同的生命周期。(例如,它们可能被相同的类加载器(loader)所加载)那么我们不考虑 MyCls2 何时被卸载及之后当MyCls和它的native方法实现 Java_mypkg_MyCls 保持使用时的重新载入。 如果这些发生,我们理应检查缓存的弱全局引用是指向一个有效的类对象还是已经被garbage收集的类对象。下一节将解释如何对弱全局引用进行那样的检查。

5.1.4 比较引用

    给定两个引用,你可以用检查 IsSameObject 函数它们是否指向相同对象,例如:
 (*env)->IsSameObject(env, obj1, obj2)
    如果obj1 和 obj2指向相同对象,函数返回 JNI_TRUE (或 1),否则返回 JNI_FALSE (或 0)。
    Java虚拟机中的JNI 的 NULL 引用指向 null 对象。如果 obj 是一个局部或一个全局引用,你可以用 
 (*env)->IsSameObject(env, obj, NULL)

 obj == NULL
来决定obj 是否指向 null 对象。
    对于弱全局引用的规则有点不同。 NULL弱全局引用指向 null 对象。但是,IsSameObject 对于弱全局引用有特殊用处。你能用 IsSameObject 来决定一个 非-NULL 弱全局引用还指向一个有效的对象。 假设 wobj 是一个 非-NULL 弱全局引用。以下调用: 
 (*env)->IsSameObject(env, wobj, NULL)
如果 wobj 指向一个已经被收集的对象,还是会返回 JNI_TRUE 并且如果 wobj 还指向有效对象,返回 JNI_FALSE。 

5.2 释放引用

    每个 JNI 引用本身消耗一定量的内存,还有被指向的对象占用的内存。作为一个 JNI 程序员,你应该注意你的程序在给定时间中奖使用的引用数量。特别地,你应该小心你的程序在执行期间任何时候能创建局部引用的数量上界,即使这些局部引用最终将被虚拟机自动释放。因为创建过量的引用,即便是临时的,也会导致内存耗尽。 

5.2.1 释放局部引用

    在很多情况下,你不必担心在执行一个native方法时释放局部引用。当native方法返回到调用者时,Java虚拟机会为你释放它们。然而,有很多场合你还是应该显式释放全局引用以避免过度内存使用。考虑以下情况:
  • 你需要在单个native方法调用中创建大量局部引用。这可能导致内部 JNI 局部引用表的溢出。及时删除那些不需要的局部引用是个好主意。例如,下面程序段中的native代码遍历一个潜在的大字符串数组。每次遍历后,native代码应该显式释放对字符串元素的局部引用,如下:
 for (i = 0; i < len; i++) {
     jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
     ... /* process jstr */
     (*env)->DeleteLocalRef(env, jstr);
 }
  • 你要写一个工具函数被未知上下文调用。显示在4.3节的 MyNewString 例子说明了在工具函数中使用 DeleteLocalRef 的用处来及时删除局部引用。否则在每次调用 MyNewString 函数,将有两个引用保持分配。 
  • 你的native方法根本不返回。例如,一个native方法可能输入一个无限事件分配循环。释放循环内部创建的局部引用以便他们不会无限累积而导致内存泄露 。
  • 你的 native 方法访问一个大型对象。native 方法于是在返回调用者之前执行额外的计算。对大型对象的局部引用将阻止对象被garbage回收,直到native方法返回,即使native方法其余部分不再使用这个对象。例如,以下程序段中,因为事先有对 DeleteLocalRef  的显式调用,当函数内部的执行时间比较长时,garbage收集器可能会释放 lref 指向的对象:
 /* A native method implementation */
 JNIEXPORT void JNICALL
 Java_pkg_Cls_func(JNIEnv *env, jobject this)
 {
     lref = ...              /* a large Java object */
     ...                     /* last use of lref */
     (*env)->DeleteLocalRef(env, lref);
 
     lengthyComputation();   /* may take some time */
     return;                 /* all local refs are freed */
 }

5.2.2 Java 2 SDK r1.2中的 管理局部引用

    Java 2 SDK r1.2 提供另一系列函数来管理局部引用的生命周期。这些函数是 EnsureLocalCapacity,New-LocalRef,PushLocalFrame,和 PopLocalFrame.。
    JNI 规范决定虚拟机自动保证每个native方法能创建至少16个局部引用。经验表明这个数量能满足绝大部分不与虚拟机中的对象进行交互的native方法的提供了足够的容量。然而,如果需要创建更多的局部引用,一个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 */
 }
    当然,以上版本可能会消耗更多内存。
    另外, Push/PopLocalFrame 函数让程序员可以创建作用域嵌套的局部引用。例如,我们也可以将同一段例程重写如下:
 #define N_REFS ... /* 每次遍历中使用到的局部引用的最大数目 */
 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 为特定数量的局部引用创建一个新的作用域。 PopLocalFrame 销毁最顶层作用域,释放这个作用域中的所有局部引用。
    使用 Push/PopLocalFrame 函数的优点是,他们是管理局部引用的生命周期成为可能,不必担心执行期间被创建的每个单个局部引用。在上面的例子中,如果处理 jstr 的计算创建更多的局部引用, 这些局部引用将在 PopLocalFrame 返回后被释放。
    当你期望写一个返回一个局部引用的工具函数时,NewLocalRef 函数是有用的。我们将在5.3 节演示 NewLocalRef 函数的使用。
    native 代码创建的局部引用可能会超过默认容量16或超过PushLocalFrame 和 EnsureLocalCapacity 调用中保留的容量。虚拟机实现将尝试为局部引用分配所需内存。但是,分配不能保证会成功。如果内存分配失败,虚拟机将退出。你应该为局部引用保留足够的内存并及时释放不需要的局部引用来避免这种情况的发生。
    Java 2 SDK r1.2 支持命令行选项 -verbose:jni。 当打开这一选项,虚拟机实现会报告超出保留容量的过度局部引用的创建。

5.2.3 释放全局引用

    当 native代码不再需要访问一个全局引用,你应当调用 DeleteGlobalRef 。如果没有成功调用这个函数,即使对象不再系统任何地方被使用的情况,Java虚拟机garbage也不会收集相应对象。 

    当 native代码不再需要访问一个弱全局引用,你应当调用 DeleteWeakGlobalRef。 如果没有成功调用这个函数,Java虚拟机garbage也会收集这个对象。但弱全局引用本身消耗的内存将无法回收。

5.3 管理引用的规则

    基于我们上节讨论的内容,我们现在准备检查Native 代码中管理 JNI 引用的规则。我们的目标是消除不必要的内存使用和对象占用。
    一般有两种 native 代码:直接实现 native 方法的函数 和 在任何上下文中使用的工具函数。
    当编写直接实现 native 方法的函数,你需要小心循环中的过度创建局部引用,以及native 方法中不需要返回的不必要的局部引用。对于虚拟机,用上16个以内的局部引用待native方法返回后删除是可以接受的。Native 方法调用一定不能引起全局引用或弱全局引用的累积,因为全局引用和弱全局引用不会在native方法返回后自动释放。
    在编写native工具函数时你必须注意不要造成执行路径中任何的局部引用泄露。因为一个工具函数可能在无法预料的情况下被重复调用,任何没有必要创建的引用可能造成内存溢出。

  • 当调用一个返回原始类型的工具函数,它一定不能有累积各类型引用的副作用。
  • 当调用一个返回引用类型的工具函数,它一定不能累积各类型引用,除了作为结果返回的那个引用。 

    It is acceptable for a utility function to create some global or weak global references for the purpose of caching because only the very first call creates these references. 
    如果一个工具函数返回引用,你应该将返回引用的类型作为函数规格的一部分。某些情况下不应该返回局部引用,另一些情况下不应该返回全局引用调用者需要了解工具函数返回引用的类型,以便正确管理自己的 JNI 引用。例如,以下代码反复调用工具函数 GetInfoString。 我们需要知道 GetInfoString 返回的引用类型,从而能够在每次遍历后正确释放返回的 JNI 引用。
 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 r 1.2中,NewLocalRef 函数某些时候对于确保一个工具函数总是返回一个局部引用是有用的。为了说明,让我们对MyNewString 刻意做些改动。下面版本将一个被频繁请求的字符串还存在一个全局引用中(称作 "CommonString") :
 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); // <<<<<< 注意这个返回的引用类型是局部引用
     }
 
     ... /* create the string as a local reference and store in
            result as a local reference */
     return result;
 }
    一般返回一个字符串是作为局部引用。如以前解释的那样,我们必须将缓存的字符串保存在一个全局引用中,以便能被多个native方法调用访问或多个线程调用。提请注意的行创建了一个指向全局引用 cachedString 的局部引用。作为和调用者的约定, MyNewString 总是返回一个局部引用。
    Push/PopLocalFrame 函数对于管理局部引用的生命周期特别方便。 如果你在native函数的入口调用 PushLocalFrame,就要在函数返回前调用 PopLocalFrame 来保证在函数执行期间所有局部引用都被释放。Push/PopLocalFrame 函数是很有效率的,强烈建议使用它们。
    如果你在函数入口调用 PushLocalFrame,记得在所有函数退出位置调用 PopLocalFrame 。例如,下面的函数开头调用了一次 PushLocalFrame,但需要多处调用PopLocalFrame:
 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;
 }
    没有正确放置 PopLocalFrame 调用将导致未定义行为,例如,虚拟机崩溃。
    以上例程也说明了为什么指定 PopLocalFrame 第二个参数是有用的。结果局部引用最初在PushLocalFrame构建的新框架中被创建。 PopLocalFrame 在弹出顶层框架之前在前一框架中转换其第二个参数(result)为一个新的局部引用。

小贴士:  JNIEnv *pEnv 是不能被直接保存起来供回调函数或线程使用的, 而 JavaVM 是可以的,所以需要用 *pEnv)->GetJavaVM 获得 JavaVM 并保存。然后在适当地方 用JavaVM -> AttachCurrentThread 函数取回 JNIEnv。


  翻译中使用特定词汇个人理解和译法很可能有差异,可能您对这样的译法有不同的想法,仅希望这张词汇表对您的理解有所帮助:

词汇表

F

function : 函数 / 功能

M

method : 方法

O

overflow : 溢出

R

reference :引用

local references : 局部引用

global references : 全局引用

weak global references:弱全局引用


你可能感兴趣的:(java,虚拟机,android,APP,jni,native)