JNI学习笔记——局部和全局引用

转载 2012年03月26日 22:51:04

JNI将实例、数组类型暴露为不透明的引用。native代码从不会直接检查一个不透明的引用指针的上下文,而是通过使用JNI函数来访问由不透明的引用所指向的数据结构。因为只处理不透明的引用,这样就不需要担心不同的java VM实现而导致的不同的内部对象的布局。然而,还是有必要了解一下JNI中不同种类的引用:

1)JNI 支持3中不透明的引用:局部引用、全局引用和弱全局引用。

2)局部和全局引用,有着各自不同的生命周期。局部引用能够被自动释放,而全局引用和若全局引用在被程序员释放之前,是一直有效的。

3)一个局部或者全局引用,使所提及的对象不能被垃圾回收。而弱全局引用,则允许提及的对象进行垃圾回收。

4)不是所有的引用都可以在所有上下文中使用的。例如:在一个创建返回引用native方法之后,使用一个局部引用,这是非法的。



局部和全局引用


那么到底什么是局部引用,什么事全局引用,它们有什么不同?



局部引用


多数JNI函数都创建局部引用。例如JNI函数NewObject创建一个实例,并且返回一个指向该实例的局部引用。

局部引用只在创建它的native方法的动态上下文中有效,并且只在native方法的一次调用中有效。所有局部引用只在一个native方法的执行期间有效,在该方法返回时,它就被回收。

在native方法中使用一个静态变量来保存一个局部引用,以便在随后的调用中使用该局部引用,这种方式是行不通的。例如以下例子,误用了局部引用:
  1. /* This code is illegal */  
  2. jstring  
  3. MyNewString(JNIEnv *env, jchar *chars, jint len)  
  4. {  
  5.     static jclass stringClass = NULL;  
  6.     jmethodID cid;  
  7.     jcharArray elemArr;  
  8.     jstring result;  
  9.     if (stringClass == NULL) {  
  10.         stringClass = (*env)->FindClass(env, "java/lang/String");  
  11.         if (stringClass == NULL) {  
  12.             return NULL; /* exception thrown */  
  13.         }  
  14.     }  
  15.     /* It is wrong to use the cached stringClass here, 
  16.        because it may be invalid. */  
  17.     cid = (*env)->GetMethodID(env, stringClass, "<init>""([C)V");  
  18.     ...  
  19.     elemArr = (*env)->NewCharArray(env, len);  
  20.     ...  
  21.     result = (*env)->NewObject(env, stringClass, cid, elemArr);  
  22.     (*env)->DeleteLocalRef(env, elemArr);  
  23.     return result;  
  24. }  
这种保存局部引用的方式是不正确的,因为FindClass()返回的是对java.lang.String的局部引用。这是因为,在native代码从MyNewString返回退出时,VM 会释放所有局部引用,包括存储在stringClass变量中的指向类对象的引用。这样当再次后继调用MyNewString时,可能会访问非法地址,导致内存被破坏,或者系统崩溃。

局部引用失效,有两种方式:‘
1)系统会自动释放局部变量。
2)程序员可以显示地管理局部引用的生命周期,例如调用DeleteLocalRef。

一个局部引用可能在被摧毁之前,被传给多个native方法。例如,MyNewString中,返回一个由NewObject创建的字符串引用,它将由NewObject的调用者来决定是否释放该引用。而在以下代码中:
  1. JNIEXPORT jstring JNICALL Java_C_f(JNIEnv *env, jobject this) {  
  2.       char *c_str = ...<pre name="code" class="cpp">      ... <pre name="code" class="cpp">return MyNewString(c_str);<pre name="code" class="cpp">}  



在VM接收到来自Java_C_f的局部引用以后,将基础字符串对象传递给ava_C_f的调用者,然后摧毁原本由MyNewString中调用的JNI函数NewObject所创建的局部引用。

局部对象只属于创建它们的线程,只在该线程中有效。一个线程想要调用另一个线程创建的局部引用是不被允许的。将一个局部引用保存到全局变量中,然后在其它线程中使用它,这是一种错误的编程。



全局引用


在一个native方法被多次调用之间,可以使用一个全局引用跨越它们。一个全局引用可以跨越多个线程,并且在被程序员释放之前,一致有效。和局部引用一样,全局引用保证了所引用的对象不会被垃圾回收。

和局部引用不一样(局部变量可以由多数JNI函数创建),全局引用只能由一个JNI函数创建(NewGlobalRef)。下面是一个使用全局引用版本的MyNewString:
  1. /* This code is OK */  
  2. jstring  
  3. MyNewString(JNIEnv *env, jchar *chars, jint len)  
  4. {  
  5.     static jclass stringClass = NULL;  
  6.     ...  
  7.     if (stringClass == NULL) {  
  8.         jclass localRefCls =  
  9.             (*env)->FindClass(env, "java/lang/String");  
  10.         if (localRefCls == NULL) {  
  11.             return NULL; /* exception thrown */  
  12.         }  
  13.         /* Create a global reference */  
  14.         stringClass = (*env)->NewGlobalRef(env, localRefCls);  
  15.         /* The local reference is no longer useful */  
  16.         (*env)->DeleteLocalRef(env, localRefCls);  
  17.         /* Is the global reference created successfully? */  
  18.         if (stringClass == NULL) {  
  19.             return NULL; /* out of memory exception thrown */  
  20.         }  
  21.     }  
  22.     ...  
  23. }  



弱全局引用


弱全局引用是在java 2 SDK1.2才出现的。它由NewGolableWeakRef函数创建,并且被DeleteGloablWeakRef函数摧毁。和全局引用一样,它可以跨native方法调用,也可以跨越不同线程。但是和全局引用不同的是,它不阻止对基础对象的垃圾回收。下面是弱全局引用版的MyNewString:
  1. JNIEXPORT void JNICALL  
  2. Java_mypkg_MyCls_f(JNIEnv *env, jobject self)  
  3. {  
  4.     static jclass myCls2 = NULL;  
  5.     if (myCls2 == NULL) {  
  6.         jclass myCls2Local =  
  7.             (*env)->FindClass(env, "mypkg/MyCls2");  
  8.         if (myCls2Local == NULL) {  
  9.             return/* can’t find class */  
  10.         }  
  11.         myCls2 = NewWeakGlobalRef(env, myCls2Local);  
  12.         if (myCls2 == NULL) {  
  13.             return/* out of memory */  
  14.         }  
  15.     }  
  16.     ... /* use myCls2 */  
  17. }  
弱全局引用在一个被native代码缓存着的引用不想阻止基础对象被垃圾回收时,非常有用。如以上例子,mypkg.MyCls.f需要缓存mypkg.MyCls2的引用。而通过将mypkg.MyCls2缓存到弱引用中,能够实现MyCls2类依旧可以被卸载。

上面代码中,我们假设了MyCls类和MyCls2类的生命周期是相同的(例如,在同一个类中被加载、卸载)。所以没有考虑MyCls2被卸载了,然后在类MyCls和native方法的实现Java_mypkg_MyCls_f还要被继续使用时,再被重新加载起来的情况。针对于这个MyCls2类可能被卸载再加载的情况,在使用时,需要检查该弱全局引用是否还有效。如何检查,这将在下面提到。



比较引用


可以用JNI函数IsSameObject来检查给定的两个局部引用、全局引用或者弱全局引用,是否指向同一个对象。
  1. (*env)->IsSameObject(env, obj1, obj2)  
返回值为:
JNI_TRUE,表示两个对象一致,是同一个对象。
JNI_FALSE,表示两个对象不一致,不是同一个对象。

在java VM中NULL是null的引用。
如果一个对象obj是局部引用或者全局引用,则可以这样来检查它是否指向null对象:
  1. (*env)->IsSameObject(env, obj, NULL)  
或者:
  1. NULL == obj  

而对于弱全局引用,以上规则需要改变一下:
我们可以用这个函数来判断一个非0弱全局引用wobj所指向的对象是否仍旧存活着(依旧有效)。
  1. (*env)->IsSameObject(env, wobj, NULL)  
返回值:
JNI_TRUE,表示对象已经被回收了。
JNI_FALSE,表示wobj指向的对象,依旧有效。


释放引用

除了引用的对象要占用内存,每个JNI引用本身也会消耗一定内存。作为一个JNI程序员,应该对在一段给定的时间里,程序会用到的引用的个数,做到心中有数。特别是,尽管程序所创建的局部引用最终会被VM会被自动地释放,仍旧需要知道在程序在执行期间的任何时刻,创建的局部引用的上限个数。创建过多的引用,即便他们是瞬间、短暂的,也会导致内存耗尽。

释放局部引用

多数情况下,在执行一个native方法时,你不需要担心局部引用的释放,java VM会在native方法返回调用者的时候释放。然而有时候需要JNI程序员显示的释放局部引用,来避免过高的内存使用。那么什么时候需要显示的释放呢,且看一下情景:
1)在单个native方法调用中,创建了大量的局部引用。这可能会导致JNI局部引用表溢出。此时有必要及时地删除那些不再被使用的局部引用。例如以下代码,在该循环中,每次都有可能创建一个巨大的字符串数组。在每个迭代之后,native代码需要显示地释放指向字符串元素的局部引用:

  1. for (i = 0; i < len; i++) {  
  2.     jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);  
  3.     ... /* process jstr */  
  4.     (*env)->DeleteLocalRef(env, jstr);  
  5. }  

2)你可能要创建一个工具函数,它会被未知的上下文调用。例如之前提到到MyNewString这个例子,它在每次返回调用者欠,都及时地将局部引用释放。

3)native方法,可能不会返回(例如,一个可能进入无限事件分发的循环中的方法)。此时在循环中释放局部引用,是至关重要的,这样才能不会无限期地累积,进而导致内存泄露。

4)native方法可能访问一个巨大的对象,因此,创建了一个指向该对象的局部引用。native方法在返回调用者之前,除访问对象之外,还执行了额外的计算。指向这个大对象的局部引用,将会包含该对象,以防被垃圾回收。这个现象会持续到native方法返回到调用者时,即便这个对象不会再被使用,也依旧会受保护。在以下例子中,由于在lengthyComputation()前,显示地调用了DeleteLocalRef,所以垃圾回收器有机会可以释放lref所指向的对象。
  1. /* A native method implementation */  
  2. JNIEXPORT void JNICALL  
  3. Java_pkg_Cls_func(JNIEnv *env, jobject this)  
  4. {  
  5.     lref = ...              /* a large Java object */  
  6.     ...                     /* last use of lref */  
  7.     (*env)->DeleteLocalRef(env, lref);  
  8.     lengthyComputation();   /* may take some time */  
  9.     return;                 /* all local refs are freed */  
  10. }  
这个情形的实质,就是允许程序在native方法执行期间,java的垃圾回收机制有机会回收native代码不在访问的对象。

在java 2 SDK1.2中管理局部引用


不知道java 7怎么样了,应该更强大吧,有时间,去看看,这里且按照java2的特性来吧。

SDK1.2中提供了一组额外的函数来管理局部引用的生命周期。他们是EnsureLocalCapacity、NewLocalRef、PushLocalFram以及PopLocalFram。

JNI的规范要求VM可以自动确保每个native方法可以创建至少16个局部引用。经验显示,如果native方法中未包含和java VM的对象进行复杂的互相操作,这个容量对大多数native方法而言,已经足够了。如果,出现这还不够的情况,需要创建更多的局部引用,那么native方法可以调用EnsureLocalCapacity来保证这些局部引用有足够的空间。
  1. /* The number of local references to be created is equal to 
  2.    the length of the array. */  
  3. if ((*env)->EnsureLocalCapacity(env, len)) < 0) {  
  4.     ... /* out of memory */  
  5. }  
  6. for (i = 0; i < len; i++) {  
  7.     jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);  
  8.     ... /* process jstr */  
  9.     /* DeleteLocalRef is no longer necessary */  
  10. }  
这样做,所消耗的内存,自然就有可能比之前的版本来的多。

另外,PushLocalFram\PopLocalFram函数允许程序员创建嵌套作用域的局部引用。如下代码:
  1. #define N_REFS ... /* the maximum number of local references  
  2.   used in each iteration */  
  3. for (i = 0; i < len; i++) {  
  4.     if ((*env)->PushLocalFrame(env, N_REFS) < 0) {  
  5.         ... /* out of memory */  
  6.     }  
  7.     jstr = (*env)->GetObjectArrayElement(env, arr, i);  
  8.     ... /* process jstr */  
  9.     (*env)->PopLocalFrame(env, NULL);  
  10. }  
PushLocalFram为指定数目的局部引用,创建一个新的作用域,PopLocalFram摧毁最上层的作用域,并且释放该域中的所有局部引用。

使用这两个函数的好处是它们可以管理局部引用的生命周期,而不需关系在执行过程中可能被创建的每个单独局部引用。例子中,如果处理jstr的过程,创建了额外的局部引用,它们也会在PopLocalFram之后被立即释放。

NewLocalRef函数,在你写一个工具函数时,非常有用。这个会在下面章节——管理引用的规则,具体分析。

native代码可能会创建超出16个局部引用的范围,也可能将他们保存在PushLocalFram或者EnsureLocalCapacity调用,VM会为局部引用分配所需要的内存。然而,这些内存是否足够,是没有保证的。如果内存分配失败,虚拟机将会退出。


释放全局引用


在native代码不再需要访问一个全局引用的时候,应该调用DeleteGlobalRef来释放它。如果调用这个函数失败,Java VM将不会回收对应的对象。

在native代码不在需要访问一个弱全局引用的时候,应该调用DeleteWeakGlobalRef来释放它。如果调用这个函数失败了,java VM 仍旧将会回收对应的底层对象,但是,不会回收这个弱引用本身所消耗掉的内存。



管理引用的规则

管理引用的目的是为了清除不需要的内存占用和对象保留。

总体来说,只有两种类型的native代码:直接实现native方法的函数,在二进制上下文中被使用的工具函数。

在写native方法的实现的时候,需要当心在循环中过度创建局部引用,以及在native方法中被创建的,却不返回给调用者的局部引用。在native方法方法返回后还留有16个局部引用在使用中,将它们交给java VM来释放,这是可以接受的。但是native方法的调用,不应该引起全局引用和弱全局引用的累积。应为这些引用不会在native方法返后被自动地释放。

在写工具函数的时候,必须要注意不能泄露任何局部引用或者超出该函数之外的执行。因为一个工具函数,可能在意料之外的上下文中,被不停的重复调用。任何不需要的引用创建都有可能导致内存泄露。
1)当一个返回一个基础类型的工具函数被调用,它必须应该没有局部引用、若全局引用的累积。
2)当一个返回一个引用类型的工具函数被调用,它必须应该没有局部、全局或若全局引用的累积,除了要被作为返回值的引用。

一个工具函数以捕获为目的创建一些全局或者弱全局引用,这是可接受的,因为只有在最开始的时候,才会创建这些引用。

如果一个工具函数返回一个引用,你应该使返回的引用的类型(例如局部引用、全局引用)作为函数规范的一部分。它应该始终如一,而不是有时候返回一个局部引用,有时候却返回一个全局引用。调用者需要知道工具函数返回的引用的类型,以便正确地管理自己的JNI引用。以下代码重复地调用一个工具工具函数(GetInfoString)。我们需要知道GetInfoString返回的引用的类型,以便释放该引用:
  1. while (JNI_TRUE) {  
  2.     jstring infoString = GetInfoString(info);  
  3.     ... /* process infoString */  
  4.     ??? /* we need to call DeleteLocalRef, DeleteGlobalRef, 
  5.   or DeleteWeakGlobalRef depending on the type of 
  6.   reference returned by GetInfoString. */  
  7. }  

在java2 SDK1.2中,NewLocalRef函数可以用来保证一个工具函数一直返回一个局部引用。为了说明这个问题,我们对MyNewString做一些改动,它缓存了一个被频繁请求的字符串(“CommonString”)到全局引用:
  1. jstring  
  2. MyNewString(JNIEnv *env, jchar *chars, jint len)  
  3. {  
  4.     static jstring result;  
  5.     /* wstrncmp compares two Unicode strings */  
  6.     if (wstrncmp("CommonString", chars, len) == 0) {  
  7.         /* refers to the global ref caching "CommonString" */  
  8.         static jstring cachedString = NULL;  
  9.         if (cachedString == NULL) {  
  10.             /* create cachedString for the first time */  
  11.             jstring cachedStringLocal = ... ;  
  12.             /* cache the result in a global reference */  
  13.             cachedString =  
  14.                 (*env)->NewGlobalRef(env, cachedStringLocal);  
  15.         }  
  16.         return (*env)->NewLocalRef(env, cachedString);  
  17.     }  
  18.     ... /* create the string as a local reference and store in 
  19.   result as a local reference */  
  20.     return result;  
  21. }  
正常的流程返回的时候局部引用。就像之前解释的那样,我们必须将缓存字符保存到一个全局引用中,这样就可以在多个线程中调用native方法时,都能访问它。
  1. return (*env)->NewLocalRef(env, cachedString);  
这条语句,创建了一个局部引用,它指向了缓存在全局引用的指向的统一对象。作为和调用者的约定的一部分,MyNewString总是返回一个局部引用。

PushLocalFram、PopLocalFram函数用来管理局部引用的生命周期特别得方便。只需要在native函数的入口调用PushLocalFram,在函数退出时调用PopLocalFram,局部变量就会被释放。
  1. jobject f(JNIEnv *env, ...)  
  2. {  
  3.     jobject result;  
  4.     if ((*env)->PushLocalFrame(env, 10) < 0) {  
  5.         /* frame not pushed, no PopLocalFrame needed */  
  6.         return NULL;  
  7.     }  
  8.     ...  
  9.     result = ...;  
  10.     if (...) {  
  11.         /* remember to pop local frame before return */  
  12.         result = (*env)->PopLocalFrame(env, result);  
  13.         return result;  
  14.     }  
  15.     ...  
  16.     result = (*env)->PopLocalFrame(env, result);  
  17.     /* normal return */  
  18.     return result;  
  19. }  
PopLocalFram函数调用失败时,可能会导致未定义的行为,例如VM崩溃。

相关文章推荐

JNI 实战全面解析

项目决定移植一款C++开源项目到Android平台,开始对JNI深入研究。 JNI是什么? JNI(Java Native Interface)意为JAVA本地调用,它允许Java代码和其他语言写的代...

JNI 常用API

利用JNIEnv自变量,程序员可访问一系列函数。这些函数可划分为下述类别: ■获取版本信息 ■进行类和对象操作 ■控制对Java对象的全局和局部引用 ■访问实例字段和静态字段 ...

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

Java中JNI的使用详解第六篇:C/C++中的引用类型和Id的缓存

首先来看一下C/C++中的引用 从Java虚拟机创建的对象传到本地C/C++代码时会产生引用,根据Java的垃圾回收机制,只要有引用存在就不会触发该引用指向的Java对象的垃圾回收 第一、...

jmethodID的NewGlobalRef(NewGlobalRef for jmethodID)和DeleteLocalRef

一开始我以为所有的引用在JNI中都是分成三个等级的引用分类。我的需求是为了提高全局jmethodID 变量在一个函数执行完后还可以用。所以用下面这个方法希望可以转换为全局引用。jobject ...
  • JQ_AK47
  • JQ_AK47
  • 2016年12月03日 22:43
  • 499

JNI局部变量和全局变量引用

1.局部变量引用 定义在本地方法内的局部变量,会随着方法调用完return后,局部变量也会随着被释放。 所以,不要在本地方法中定义static变量来存储数据!举个栗子: 像上面这样子是不行滴!...
  • fwt336
  • fwt336
  • 2017年03月08日 11:04
  • 1005

JNI/NDK开发指南(十)——JNI局部引用、全局引用和弱全局引用

转载请注明出处:http://blog.csdn.net/xyang81/article/details/44657385    这篇文章比较偏理论,详细介绍了在编写本地代码时三种引用的使用场景和注意...
  • xyang81
  • xyang81
  • 2015年04月04日 16:28
  • 8029

jni中的本地引用和全局引用

JNI支持三种类型的java对象引用:局部引用(local reference)、全局引用(global reference)以及弱全局引用(weak global reference)。三种类型的引...

JNI官方规范中文版——如何使用JNI中的global reference和local reference

JNI提供了一些实例和数组类型(jobject、jclass、jstring、jarray等)作为不透明的引用供本地代码使用。本地代码永远不会直接操作引用指向的VM内部的数据内容。要进行这些操作,必须...

Java中JNI的使用详解第二篇:JNIEnv类型和jobject类型的解释

上一篇说的是一个简单的应用,说明JNI是怎么工作的,这一篇主要来说一下,那个本地方法sayHello的参数的说明,以及其中方法的使用 首先来看一下C++中的sayHello方法的实现: JNIEXPO...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:JNI学习笔记——局部和全局引用
举报原因:
原因补充:

(最多只允许输入30个字)