文章目录
JNI的局部引用()溢出问题原因探究和修复的方法
当深入使用JNI的时候,局部引用溢出的问题还是很容易遇到的。让我们一起来探究原因和修复方法
什么是局部引用?
局部引用:当在Native层调用JNI函数(例如:FindClass、NewObject、GetObjectClass和NewCharArray等等)创建Java对象时,该Java对象会被添加到局部引用表中。通过添加到局部引用表中,可以阻止GC回收局部引用表引用的对象。当从Native函数返回Java层后,局部引用所引用的对象会被JVM自动释放。当然也可以手动调用DeleteLocalRef来释放局部引用。
(*env)->DeleteLocalRef(env,local_ref)
。
局部引用有如下特点:
- Native方法中的局部引用有效期是Native方法的调用期间。
- 调用期间产生的局部引用除非手动删除,不然就会一直累加
- 在Native方法中创建子线程并运行,然后在子线程中通过JNI函数创建的Java对象所产生的局部引用只有在这个线程结束运行才会自动删除。
局部引用溢出原因是什么?
通过上面的特点说明,可以总结出两种主要的泄露情况:
1. 在Native函数中,通过调用JNI函数创建大量Java对象
例如下面这个Native函数,创建了一个ArrayList,并往里面填充1000个String对象。由于每一次调用NewStringUTF
都会添加一个局部引用到局部引用表中
JNIEXPORT jobject JNICALL
Java_com_kevin_localreferencetableoverflow_LRTOverflow_createArrayListCauseLRTOverflow(JNIEnv *env,
jclass clazz) {
jclass java_util_ArrayList_class;
jmethodID java_util_ArrayList_;
jmethodID java_util_ArrayList_add;
java_util_ArrayList_class = env->FindClass("java/util/ArrayList");
java_util_ArrayList_ = env->GetMethodID(java_util_ArrayList_class, "<init>", "()V");
java_util_ArrayList_add = env->GetMethodID(java_util_ArrayList_class, "add", "(Ljava/lang/Object;)Z");
jobject result = env->NewObject(java_util_ArrayList_class, java_util_ArrayList_);
for (int i=0; i <= 1000; i++) {
std::string hello("Hello");
hello.append(std::to_string(i));
jstring element = env->NewStringUTF(hello.c_str());
env->CallBooleanMethod(result, java_util_ArrayList_add, element);
//env->DeleteLocalRef(element);//手动删除局部引用可以避免local reference table overflow
}
//printDumpReferenceTables(env);//打印所有局部引用表
return result;
}
局部引用会在JNI函数返回前添加到局部引用表。参考如下NewStringUTF
实现代码,类似的创建java对象的jni函数都会执行类似的操作
art/runtime/jni_internal.cc
static jstring NewStringUTF(JNIEnv* env, const char* utf) {
if (utf == nullptr) {
return nullptr;
}
ScopedObjectAccess soa(env);
mirror::String* result = mirror::String::AllocFromModifiedUtf8(soa.Self(), utf);
return soa.AddLocalReference<jstring>(result);//添加到局部引用表中
}
2. 在Native子线程中,通过调用JNI函数创建大量Java对象
下面是一个Native子线程中创建大量Java对象的例子。
在Native中创建一个线程,然后该线程内部运行一个while(1)循环。每循环一次就会创建一个jstring。
这种场景往往出现在使用非阻塞网络库接收服务器发送的消息,并将消息转换成jstring传递给JVM
JavaVM *javaVM;
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
javaVM = vm;
//init();
return JNI_VERSION_1_4;
}
static std::thread *t1;
int count;
static void call_from_thread() {
JNIEnv *env;
if (javaVM->AttachCurrentThread(&env, NULL) != JNI_OK) {
return;
}
std::string hello("Hello");
while (1) {
jstring element = env->NewStringUTF(hello.c_str());
//env->DeleteLocalRef(element);//手动删除局部引用可以避免local reference table overflow
}
printDumpReferenceTables(env);
}
void JNICALL
Java_com_kevin_localreferencetableoverflow_LRTOverflow_createLoopThreadCauseLRTOverflow(JNIEnv *env,
jclass clazz) {
t1 = new std::thread(call_from_thread);//创建并执行一个子线程
//printDumpReferenceTables(env);//打印所有局部引用表
}
补充说明
- 局部引用溢出在Android不同版本上表现有所区别。
Android 8.0 之前局部引用表的上限是512个引用,Android 8.0后局部引用表上限提升到了8388608个引用,所以要测试溢出,需要注意系统版本。 - Oracle Java 没有局部引用表上限限制,随着局部引用表不断增大,最终会出现OOM。
局部引用表溢出调试验证
上面的代码是从我的测试Demo中摘录的,如果亲自实验并尝试,可以git clone LocalReferenceTableOverflow源码测试验证。