JNI支持三种类型的java对象引用:局部引用(local reference)、全局引用(global reference)以及弱全局引用(weak global reference)。三种类型的引用具有不同的生命周期,另外垃圾回收器对这三种对象引用的管理方式也不同。创建局部引用的本地方法返回后(注意:这里是指返回到java方法),局部引用将变成无效。而全局引用以及弱全局引用在本地方法返回后,仍然有效。垃圾回收器无法回收本地方法创建的局部引用和全局引用,但可以回收本地方法创建的弱全局引用。为了编写正确而且内存安全的java本地方法,很有必要对这三种类型的对象引用做些总结。
一 局部引用的注意事项
大多数jni方法创建的对象引用都属于局部引用(local reference),如FindClass方法、NewObject等。局部引用不能作为静态变量,也不能作为全局变量。如下一段程序就是错误的:
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是危险的,因为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;
}
虚拟机中,每一个方法都对应着执行栈(interpreter stack)中的一个帧(frame)。Java方法对应着Java帧(Java Frame),本地方法对应着转变帧(Transition Frame)。局部引用是保存在帧栈(stack Of frame)中,当方法返回时帧就会从执行栈中弹出。因此,帧栈中所有的引用就全部变成无效。在本地方法返回前,局部引用的内存是不能被垃圾回收器回收的。如果一个本地方法中存在循环且循环体中会创建局部引用,那么很有可能导致outOfMemeroy异常。为了保证内存安全,程序员应该在循环体中调用DeleteLocalRef来释放局部引用。
不同的虚拟机实现垃圾回收的算法并不相同,传统上有两种方法:引用计数法、触及法。引用计数法无法检测出循环引用,现在已经很少使用。触及法的总体思路是:首先创建一个根集合,该集合中的对象是肯定处于活跃态的;然后从根集合开始做遍历,所有可被遍历到的对象都被认为是处于活跃态的;最后,非活跃态的对象被回收。帧栈中的引用会在垃圾回收的第一步中被加入根集合,所以在执行栈将局部引用所属帧弹出前,局部引用是肯定不会被垃圾回收器回收的。
二 全局引用的注意事项
只有一个jni方法可以创建全局引用:NewGlobalRef。下面给出了一段创建全局引用的程序:
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 */
}
/* 创建一个全局引用 */
stringClass = (*env)->NewGlobalRef(env, localRefCls);
/* 局部引用localRefCls不再有效,删除局部引用localRefCls*/
(*env)->DeleteLocalRef(env, localRefCls);
if (stringClass == NULL) {
return NULL; /* out of memory exception thrown */
}
}
...
}
与局部引用类似的是,在垃圾回收的第一个过程中全局引用也会被加入到根集合中。而且即使创建全局引用的帧从执行栈中弹出后,全局引用仍然有效。所以,释放全局引用的工作必须由程序员完成。调用DeleteGlobalRef来释放全局引用。
三 弱全局引用的注意事项
创建弱全局引用的接口是:NewGlobalWeakRef。释放弱全局引用的接口是:DeleteWeakGlobalRef(《jni手册》中对这个接口的前后描述不一致,我是对比了phoneME虚拟机的)。在本地方法返回后,弱全局引用仍然有效。另外,在垃圾回收期运行的第一步中,弱全局对象不会被加入根集合。因此,弱全局引用可以被垃圾回收器回收。下面给出了一段使用弱全局引用的程序:
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 */
}
对于上面的例程,本地方法f属于类java.mypkg.MyCls。可以认为引用myCls2的生命周期与类java.mypkg.MyCls相同。
参考资料:
1. 《jni程序员手册》
2. 《深入Java虚拟机》
3. 《phoneME-advance-mr2》源码
一 局部引用的注意事项
大多数jni方法创建的对象引用都属于局部引用(local reference),如FindClass方法、NewObject等。局部引用不能作为静态变量,也不能作为全局变量。如下一段程序就是错误的:
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是危险的,因为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;
}
虚拟机中,每一个方法都对应着执行栈(interpreter stack)中的一个帧(frame)。Java方法对应着Java帧(Java Frame),本地方法对应着转变帧(Transition Frame)。局部引用是保存在帧栈(stack Of frame)中,当方法返回时帧就会从执行栈中弹出。因此,帧栈中所有的引用就全部变成无效。在本地方法返回前,局部引用的内存是不能被垃圾回收器回收的。如果一个本地方法中存在循环且循环体中会创建局部引用,那么很有可能导致outOfMemeroy异常。为了保证内存安全,程序员应该在循环体中调用DeleteLocalRef来释放局部引用。
不同的虚拟机实现垃圾回收的算法并不相同,传统上有两种方法:引用计数法、触及法。引用计数法无法检测出循环引用,现在已经很少使用。触及法的总体思路是:首先创建一个根集合,该集合中的对象是肯定处于活跃态的;然后从根集合开始做遍历,所有可被遍历到的对象都被认为是处于活跃态的;最后,非活跃态的对象被回收。帧栈中的引用会在垃圾回收的第一步中被加入根集合,所以在执行栈将局部引用所属帧弹出前,局部引用是肯定不会被垃圾回收器回收的。
二 全局引用的注意事项
只有一个jni方法可以创建全局引用:NewGlobalRef。下面给出了一段创建全局引用的程序:
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 */
}
/* 创建一个全局引用 */
stringClass = (*env)->NewGlobalRef(env, localRefCls);
/* 局部引用localRefCls不再有效,删除局部引用localRefCls*/
(*env)->DeleteLocalRef(env, localRefCls);
if (stringClass == NULL) {
return NULL; /* out of memory exception thrown */
}
}
...
}
与局部引用类似的是,在垃圾回收的第一个过程中全局引用也会被加入到根集合中。而且即使创建全局引用的帧从执行栈中弹出后,全局引用仍然有效。所以,释放全局引用的工作必须由程序员完成。调用DeleteGlobalRef来释放全局引用。
三 弱全局引用的注意事项
创建弱全局引用的接口是:NewGlobalWeakRef。释放弱全局引用的接口是:DeleteWeakGlobalRef(《jni手册》中对这个接口的前后描述不一致,我是对比了phoneME虚拟机的)。在本地方法返回后,弱全局引用仍然有效。另外,在垃圾回收期运行的第一步中,弱全局对象不会被加入根集合。因此,弱全局引用可以被垃圾回收器回收。下面给出了一段使用弱全局引用的程序:
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 */
}
对于上面的例程,本地方法f属于类java.mypkg.MyCls。可以认为引用myCls2的生命周期与类java.mypkg.MyCls相同。
参考资料:
1. 《jni程序员手册》
2. 《深入Java虚拟机》
3. 《phoneME-advance-mr2》源码