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源碼測試驗證。

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