Android JNI 局部引用溢出(local reference table overflow (max=512))

JNI的局部引用()溢出问题原因探究和修复的方法

当深入使用JNI的时候,局部引用溢出的问题还是很容易遇到的。让我们一起来探究原因和修复方法

什么是局部引用?

局部引用:当在Native层调用JNI函数(例如:FindClass、NewObject、GetObjectClass和NewCharArray等等)创建Java对象时,该Java对象会被添加到局部引用表中。通过添加到局部引用表中,可以阻止GC回收局部引用表引用的对象。当从Native函数返回Java层后,局部引用所引用的对象会被JVM自动释放。当然也可以手动调用DeleteLocalRef来释放局部引用。(*env)->DeleteLocalRef(env,local_ref)

局部引用有如下特点:

  1. Native方法中的局部引用有效期是Native方法的调用期间。
  2. 调用期间产生的局部引用除非手动删除,不然就会一直累加
  3. 在Native方法中创建子线程并运行,然后在子线程中通过JNI函数创建的Java对象所产生的局部引用只有在这个线程结束运行才会自动删除。

局部引用溢出原因是什么?

通过上面的特点说明,可以总结出两种主要的泄露情况:

1. 在Native函数中,通过调用JNI函数创建大量Java对象

例如下面这个Native函数,创建了一个ArrayList,并往里面填充1000个String对象。由于每一次调用NewStringUTF都会添加一个局部引用到局部引用表中

LRTOverflow.cpp

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

LRTOverflow.cpp

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);//打印所有局部引用表
}

补充说明

  1. 局部引用溢出在Android不同版本上表现有所区别。
    Android 8.0 之前局部引用表的上限是512个引用,Android 8.0后局部引用表上限提升到了8388608个引用,所以要测试溢出,需要注意系统版本。
  2. Oracle Java 没有局部引用表上限限制,随着局部引用表不断增大,最终会出现OOM。

局部引用表溢出调试验证

上面的代码是从我的测试Demo中摘录的,如果亲自实验并尝试,可以git clone LocalReferenceTableOverflow源码测试验证。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章