上面代码中,我们省略了和我们讨论无关的代码。因为FindClass返回一个对java.lang.String对象的局部引用,上面代码中缓存cls_string做法是错误的。假设一个本地方法C.f调用了newString:
JNIEXPORT jstring JNICALL
Java_C_f(JNIEnv *env, jobject this)
{
char *c_str = …;
…
return newString(c_str);
}
Java_com_study_jnilearn_AccessCache_newString 下面简称newString
C.f方法返回后,JVM会释放在这个方法执行期间创建的所有局部引用,也包含对String的Class引用cls_string。当再次调用newString时,newString所指向引用的内存空间已经被释放,成为了一个野指针,再访问这个指针的引用时,会导致因非法的内存访问造成程序崩溃。
…
… = C.f(); // 第一次调是OK的
… = C.f(); // 第二次调用时,访问的是一个无效的引用.
…
释放局部引用
释放一个局部引用有两种方式,一个是本地方法执行完毕后JVM自动释放,另外一个是自己调用DeleteLocalRef手动释放。既然JVM会在函数返回后会自动释放所有局部引用,为什么还需要手动释放呢?大部分情况下,我们在实现一个本地方法时不必担心局部引用的释放问题,函数被调用完成后,JVM 会自动释放函数中创建的所有局部引用。尽管如此,以下几种情况下,为了避免内存溢出,我们应该手动释放局部引用:
1、JNI会将创建的局部引用都存储在一个局部引用表中,如果这个表超过了最大容量限制,就会造成局部引用表溢出,使程序崩溃。经测试,Android上的JNI局部引用表最大数量是512个。当我们在实现一个本地方法时,可能需要创建大量的局部引用,如果没有及时释放,就有可能导致JNI局部引用表的溢出,所以,在不需要局部引用时就立即调用DeleteLocalRef手动删除。比如,在下面的代码中,本地代码遍历一个特别大的字符串数组,每遍历一个元素,都会创建一个局部引用,当对使用完这个元素的局部引用时,就应该马上手动释放它。
for (i = 0; i < len; i++) {
jstring jstr = (env)->GetObjectArrayElement(env, arr, i);
… / 使用jstr */
(*env)->DeleteLocalRef(env, jstr); // 使用完成之后马上释放
}
2、在编写JNI工具函数时,工具函数在程序当中是公用的,被谁调用你是不知道的。上面newString这个函数演示了怎么样在工具函数中使用完局部引用后,调用DeleteLocalRef删除。不这样做的话,每次调用newString之后,都会遗留两个引用占用空间(elemArray和cls_string,cls_string不用static缓存的情况下)。
3、如果你的本地函数不会返回。比如一个接收消息的函数,里面有一个死循环,用于等待别人发送消息过来while(true) { if (有新的消息) { 处理之。。。。} else { 等待新的消息。。。}}。如果在消息循环当中创建的引用你不显示删除,很快将会造成JVM局部引用表溢出。
4、局部引用会阻止所引用的对象被GC回收。比如你写的一个本地函数中刚开始需要访问一个大对象,因此一开始就创建了一个对这个对象的引用,但在函数返回前会有一个大量的非常复杂的计算过程,而在这个计算过程当中是不需要前面创建的那个大对象的引用的。但是,在计算的过程当中,如果这个大对象的引用还没有被释放的话,会阻止GC回收这个对象,内存一直占用者,造成资源的浪费。所以这种情况下,在进行复杂计算之前就