解析Java的JNI编程中的对象引用与内存泄漏问题
JNI,Java Native Interface,是 native code 的编程接口。JNI 使 Java 代码程序可以与 native code 交互――在 Java 程序中调用 native code;在 native code 中嵌入 Java 虚拟机调用 Java 的代码。 局部和全局引用 JNI将实例、数组类型暴露为不透明的引用。native代码从不会直接检查一个不透明的引用指针的上下文,而是通过使用JNI函数来访问由不透明的引用所指向的数据结构。因为只处理不透明的引用,这样就不需要担心不同的java VM实现而导致的不同的内部对象的布局。然而,还是有必要了解一下JNI中不同种类的引用: 那么到底什么是局部引用,什么事全局引用,它们有什么不同? 局部引用 多数JNI函数都创建局部引用。例如JNI函数NewObject创建一个实例,并且返回一个指向该实例的局部引用。 局部引用只在创建它的native方法的动态上下文中有效,并且只在native方法的一次调用中有效。所有局部引用只在一个native方法的执行期间有效,在该方法返回时,它就被回收。 在native方法中使用一个静态变量来保存一个局部引用,以便在随后的调用中使用该局部引用,这种方式是行不通的。例如以下例子,误用了局部引用: 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 */ } } /* It is wrong to use the cached stringClass here,because it may be invalid. */ cid = (*env)->GetMethodID(env,stringClass,"<init>","([C)V"); ... elemArr = (*env)->NewCharArray(env,len); ... result = (*env)->NewObject(env,cid,elemArr); (*env)->DeleteLocalRef(env,elemArr); return result; } 这种保存局部引用的方式是不正确的,因为FindClass()返回的是对java.lang.String的局部引用。这是因为,在native代码从MyNewString返回退出时,VM 会释放所有局部引用,包括存储在stringClass变量中的指向类对象的引用。这样当再次后继调用MyNewString时,可能会访问非法地址,导致内存被破坏,或者系统崩溃。 局部引用失效,有两种方式:‘ 一个局部引用可能在被摧毁之前,被传给多个native方法。例如,MyNewString中,返回一个由NewObject创建的字符串引用,它将由NewObject的调用者来决定是否释放该引用。而在以下代码中: JNIEXPORT jstring JNICALL Java_C_f(JNIEnv *env,jobject this) { 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: MyNewString(JNIEnv *env,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 */ } } ... }
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 */ } 弱全局引用在一个被native代码缓存着的引用不想阻止基础对象被垃圾回收时,非常有用。如以上例子,mypkg.MyCls.f需要缓存mypkg.MyCls2的引用。而通过将mypkg.MyCls2缓存到弱引用中,能够实现MyCls2类依旧可以被卸载。
比较引用 可以用JNI函数IsSameObject来检查给定的两个局部引用、全局引用或者弱全局引用,是否指向同一个对象。
(*env)->IsSameObject(env,obj,NULL) 或者: NULL == obj
(*env)->IsSameObject(env,wobj,NULL) 返回值: 释放引用 释放局部引用 for (i = 0; i < len; i++) { jstring jstr = (*env)->GetObjectArrayElement(env,arr,i); ... /* process jstr */ (*env)->DeleteLocalRef(env,jstr); } 2)你可能要创建一个工具函数,它会被未知的上下文调用。例如之前提到到MyNewString这个例子,它在每次返回调用者欠,都及时地将局部引用释放。
/* 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 */ } 这个情形的实质,就是允许程序在native方法执行期间,java的垃圾回收机制有机会回收native代码不在访问的对象。 管理局部引用 /* 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,i); ... /* process jstr */ /* DeleteLocalRef is no longer necessary */ } 这样做,所消耗的内存,自然就有可能比之前的版本来的多。
#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) { ... /* out of memory */ } jstr = (*env)->GetObjectArrayElement(env,i); ... /* process jstr */ (*env)->PopLocalFrame(env,NULL); } PushLocalFram为指定数目的局部引用,创建一个新的作用域,PopLocalFram摧毁最上层的作用域,并且释放该域中的所有局部引用。
native代码可能会创建超出16个局部引用的范围,也可能将他们保存在PushLocalFram或者EnsureLocalCapacity调用,VM会为局部引用分配所需要的内存。然而,这些内存是否足够,是没有保证的。如果内存分配失败,虚拟机将会退出。 释放全局引用
总体来说,只有两种类型的native代码:直接实现native方法的函数,在二进制上下文中被使用的工具函数。 在写native方法的实现的时候,需要当心在循环中过度创建局部引用,以及在native方法中被创建的,却不返回给调用者的局部引用。在native方法方法返回后还留有16个局部引用在使用中,将它们交给java VM来释放,这是可以接受的。但是native方法的调用,不应该引起全局引用和弱全局引用的累积。应为这些引用不会在native方法返后被自动地释放。
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. */ } 在java2 SDK1.2中,NewLocalRef函数可以用来保证一个工具函数一直返回一个局部引用。为了说明这个问题,我们对MyNewString做一些改动,它缓存了一个被频繁请求的字符串(“CommonString”)到全局引用: jstring MyNewString(JNIEnv *env,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方法时,都能访问它。 return (*env)->NewLocalRef(env,cachedString); 这条语句,创建了一个局部引用,它指向了缓存在全局引用的指向的统一对象。作为和调用者的约定的一部分,MyNewString总是返回一个局部引用。
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; } PopLocalFram函数调用失败时,可能会导致未定义的行为,例如VM崩溃。 内存泄漏问题 JNI 编程实现了 native code 和 Java 程序的交互,因此 JNI 代码编程既遵循 native code 编程语言的编程规则,同时也遵守 JNI 编程的文档规范。在内存管理方面,native code 编程语言本身的内存管理机制依然要遵循,同时也要考虑 JNI 编程的内存管理。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |