JNI內(nèi)存管理

面試的時(shí)候遇到一些候選人的簡(jiǎn)歷上寫(xiě)著熟悉jni,但是問(wèn)的時(shí)候才發(fā)現(xiàn)對(duì)jni的了解僅僅是停留在java和c的方法是如何相互調(diào)用上。其實(shí)這遠(yuǎn)遠(yuǎn)稱(chēng)不上熟悉,這篇博客就來(lái)講講jni面試中經(jīng)常還會(huì)問(wèn)到的內(nèi)存管理問(wèn)題。

首先我們知道java和c的對(duì)象是不能直接共用的,例如字符串我們不能直接返回char*,而需要?jiǎng)?chuàng)建一個(gè)jstring對(duì)象:

std::string hello = "hello world";
jstring jstr = env->NewStringUTF(hello.c_str());

那問(wèn)題就來(lái)了,這個(gè)jstr是我們用env去new出來(lái)的。那我們需要手動(dòng)去delete嗎,不delete會(huì)不會(huì)造成內(nèi)存泄露?

如果需要的話,當(dāng)我們需要將這個(gè)jstr返回給java層使用的時(shí)候又要怎么辦呢?不delete就內(nèi)存泄露,delete就野指針:

extern "C" JNIEXPORT jstring JNICALL
Java_me_linjw_ndkdemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject thiz/* this */) {
    std::string hello = "hello world";
    jstring jstr = env->NewStringUTF(hello.c_str());
    return jstr;
}

其實(shí)jni為了解決這個(gè)問(wèn)題,設(shè)計(jì)了三種引用類(lèi)型:

  • 局部引用
  • 全局引用
  • 弱全局引用

局部引用

我們先從局部引用講起,其實(shí)我們這里通過(guò)NewStringUTF創(chuàng)建的jstring就是局部引用,那它有什么特點(diǎn)呢?

我們?cè)赾層大多數(shù)調(diào)用jni方法創(chuàng)建的引用都是局部引用,它會(huì)別存放在一張局部引用表里。它的內(nèi)存有四種釋放方式:

  1. 程序員可以手動(dòng)調(diào)用DeleteLocalRef去釋放
  2. c層方法執(zhí)行完成返回java層的時(shí)候,jvm會(huì)遍歷局部引用表去釋放
  3. 使用PushLocalFrame/PopLocalFrame創(chuàng)建/銷(xiāo)毀局部引用棧幀的時(shí)候,在PopLocalFrame里會(huì)釋放幀內(nèi)創(chuàng)建的引用
  4. 如果使用AttachCurrentThread附加原生線程,在調(diào)用DetachCurrentThread的時(shí)候會(huì)釋放該線程創(chuàng)建的局部引用

所以上面的問(wèn)題我們就能回答了, jstr可以不用手動(dòng)delete,可以等方法結(jié)束的時(shí)候jvm自己去釋放(當(dāng)然如果返回之后在java層將這個(gè)引用保存了起來(lái),那也是不會(huì)立馬釋放內(nèi)存的)

但是這樣是否就意味著我們可以任性的去new對(duì)象,不用考慮任何東西呢?其實(shí)也不是,局部引用表是有大小限制的,如果new的內(nèi)存太多的話可能造成局部引用表的內(nèi)存溢出,例如我們?cè)趂or循環(huán)里面不斷創(chuàng)建對(duì)象:

std::string hello = "hello world";
for(int i = 0 ; i < 9999999 ; i ++) {
    env->NewStringUTF(hello.c_str());
}

這就會(huì)引起local reference table overflow:

1.png

所以在使用完之后一定記得調(diào)用DeleteLocalRef去釋放它。

有些同學(xué)可能會(huì)說(shuō),怎么可能會(huì)有人真的直接就在循環(huán)里不斷創(chuàng)建對(duì)象呢。其實(shí)這種溢出大多數(shù)情況發(fā)生在被循環(huán)調(diào)用的方法里面:

void func(JNIEnv *env) {
    std::string hello = "hello world";
    env->NewStringUTF(hello.c_str());
}

...

for(int i = 0 ; i < 9999999 ; i ++) {
    func(env);
}

作為一個(gè)安全的程序員,在對(duì)象不再使用的時(shí)候,立馬使用DeleteLocalRef去將其釋放是一個(gè)很好的習(xí)慣。

局部引用棧幀

如上面所說(shuō)我們可能在某個(gè)函數(shù)中創(chuàng)建了局部引用,然后這個(gè)函數(shù)在循環(huán)中被調(diào)用,就容易出現(xiàn)溢出。

但是如果方法里面創(chuàng)建了多個(gè)局部引用,在return之前一個(gè)個(gè)去釋放會(huì)顯得十分繁瑣:

void func(JNIEnv *env) {
    ...
    jstring jstr1 = env->NewStringUTF(str1.c_str());
    jstring jstr2 = env->NewStringUTF(str2.c_str());
    jstring jstr3 = env->NewStringUTF(str3.c_str());
    jstring jstr4 = env->NewStringUTF(str4.c_str());
    ...
    env->DeleteLocalRef(jstr1);
    env->DeleteLocalRef(jstr2);
    env->DeleteLocalRef(jstr3);
    env->DeleteLocalRef(jstr4);
}

這個(gè)時(shí)候可以考慮使用局部引用棧幀:

void func(JNIEnv *env) {
    env->PushLocalFrame(4);
    ...
    jstring jstr1 = env->NewStringUTF(str1.c_str());
    jstring jstr2 = env->NewStringUTF(str2.c_str());
    jstring jstr3 = env->NewStringUTF(str3.c_str());
    jstring jstr4 = env->NewStringUTF(str4.c_str());
    ...
    env->PopLocalFrame(NULL);
}

我們?cè)诜椒ㄩ_(kāi)頭PushLocalFrame,結(jié)尾PopLocalFrame,這樣整個(gè)方法就在一個(gè)局部引用幀里面,而在PopLocalFrame就會(huì)將該幀里面創(chuàng)建的局部引用全部釋放。

有的同學(xué)可能會(huì)想到一種場(chǎng)景,如果需要將某個(gè)局部引用當(dāng)初返回值返回怎么辦?用局部引用幀會(huì)不會(huì)造成野指針?

其實(shí)jni也考慮到了這中情況,所以PopLocalFrame有一個(gè)參數(shù):

jobject PopLocalFrame(jobject result)

這個(gè)result參數(shù)可以傳入你的返回值引用,這樣的話這個(gè)局部引用就會(huì)在去到父幀里面,這樣就能直接返回了:

jstring func(JNIEnv *env) {
    env->PushLocalFrame(4);
    ...
    jstring jstr1 = env->NewStringUTF(str1.c_str());
    jstring jstr2 = env->NewStringUTF(str2.c_str());
    jstring jstr3 = env->NewStringUTF(str3.c_str());
    jstring jstr4 = env->NewStringUTF(str4.c_str());
    ...
    return (jstring)env->PopLocalFrame(jstr4);
}

PS: 就算使用了result參數(shù),局部引用幀里面的引用也是會(huì)失效的,所以不能直接將它返回,而是需要用PopLocalFrame為它創(chuàng)建的新引用,這個(gè)引用才在父幀里面。

多線程下的局部引用

前面三種情況我們好理解,但是第四種情況又是什么意思呢?

3.如果使用AttachCurrentThread附加原生線程,在調(diào)用DetachCurrentThread的時(shí)候會(huì)釋放該線程創(chuàng)建的局部引用

我們使用JNIEnv這個(gè)數(shù)據(jù)結(jié)構(gòu)去調(diào)用jni的方法創(chuàng)建局部引用,但是JNIEnv將用于線程本地存儲(chǔ),所以我們不能在線程之間共享它。

如果是java層創(chuàng)建的線程,那調(diào)到c層會(huì)自然傳入一個(gè)JNIEnv指針,但是如果是我們?cè)赾層自己新建的線程,我們要怎么拿的這個(gè)線程的JNIEnv呢?

在講解之前還有一個(gè)知識(shí)點(diǎn)要先交代,除了JNIEnv其實(shí)jni還有個(gè)很重要的數(shù)據(jù)結(jié)構(gòu)JavaVM,理論上每個(gè)進(jìn)程可以有多個(gè)JavaVM,但Android只允許有一個(gè),所以JavaVM是可以在多線程間共享的。

我們?cè)趈ava層使用System.loadLibrary方法加載so的時(shí)候,c層的JNI_OnLoad方法會(huì)被調(diào)用,我們可以在拿到JavaVM指針并將它保存起來(lái):

JavaVM* g_Vm;

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    g_Vm = vm;
    return JNI_VERSION_1_4;
}

之后可以在線程中使用它的AttachCurrentThread方法附加原生線程,然后在線程結(jié)束的時(shí)候使用DetachCurrentThread去解除附加:

pthread_t g_pthread;
JavaVM* g_vm;

void* ThreadRun(void *data) {
    JNIEnv* env;
    g_vm->AttachCurrentThread(&env, nullptr);
    ...
    g_vm->DetachCurrentThread();
}

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    g_vm = vm;
    return JNI_VERSION_1_4;
}

...

pthread_create(&g_pthread, NULL, ThreadRun, (void *) 1);

所以在AttachCurrentThread和DetachCurrentThread之間JNIEnv都是有效的,我們可以使用它去創(chuàng)建局部引用,而在DetachCurrentThread之后JNIEnv就失效了,同時(shí)我們用它創(chuàng)建的局部引用也會(huì)被回收。

全局引用

假設(shè)我們需要使用監(jiān)聽(tīng)者模式在c層保存java對(duì)象的引用,并啟動(dòng)線程執(zhí)行操作,在適當(dāng)?shù)臅r(shí)候通知java層。我們需要怎么做,一種<font color='red'>錯(cuò)誤</font>的做法是直接將傳入的jobject保存到全局變量:

jobject g_listener;

extern "C" JNIEXPORT void JNICALL
Java_me_linjw_ndkdemo_MainActivity_registerListener(
        JNIEnv *env,
        jobject thiz,
        jobject listener) {
    g_listener = listener; // 錯(cuò)誤的做法!!!
}

原因是這里傳進(jìn)來(lái)的jobject其實(shí)也是局部引用,而局部引用是不能跨線程使用的。我們應(yīng)該將它轉(zhuǎn)換成全局引用去保存:

jobject g_listener;

extern "C" JNIEXPORT void JNICALL
Java_me_linjw_ndkdemo_MainActivity_registerListener(
        JNIEnv *env,
        jobject thiz,
        jobject listener) {
    g_listener = env->NewGlobalRef(listener);
}

顧名思義,全局引用就是全局存在的引用,只有在我們調(diào)用DeleteGlobalRef之后它才會(huì)失效。

然后這樣又出現(xiàn)了個(gè)問(wèn)題,按道理這個(gè)g_listener和listener應(yīng)該指向的是同一個(gè)java對(duì)象,但是如果我們這樣去判斷的話是錯(cuò)誤的:

if(g_listener == listener) {
    ...
}

它們的值是不會(huì)相等的,如果要判斷兩個(gè)jobject是否指向同一個(gè)java對(duì)象要需要用IsSameObject去判斷:

if(env->IsSameObject(g_listener, listener)) {
    ...   
}

弱全局引用

弱全局引用和全局引用類(lèi)似,可以在跨線程使用,它使用NewGlobalWeakRef創(chuàng)建,使用DeleteGlobalWeakRef釋放。

但是弱全局引用是會(huì)被gc回收的,所以在使用的時(shí)候我們需要先判斷它是否已經(jīng)被回收:

if(!env->IsSameObject(g_listener, NULL)) {
    ...   
}

JNI中的NULL引用指向JVM中的null對(duì)象。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • *** 說(shuō)明:本文不代表博主觀點(diǎn),均是由以下資料整理的讀書(shū)筆記。 *** 【參考資料】 1、向您的Android ...
    莫緒旻_向嶼閱讀 1,313評(píng)論 0 5
  • JNI概念 JNI是本地語(yǔ)言編程接口。它允許運(yùn)行在JVM中的Java代碼和用C、C++或匯編寫(xiě)的本地代碼相互操作。...
    加油碼農(nóng)閱讀 1,158評(píng)論 0 0
  • 0.要素1.類(lèi)操作2.異常操作3.全局及局部引用4.對(duì)象操作5.字符串操作6.數(shù)組操作7.訪問(wèn)對(duì)象的屬性和方法7....
    MagicalGuy閱讀 1,455評(píng)論 0 2
  • ref: Android Studio開(kāi)發(fā)JNI示例Android NDK 開(kāi)發(fā)(二)JNI 傳遞參數(shù)和返回值A(chǔ)n...
    richy_閱讀 1,736評(píng)論 0 8
  • 今天兒子的老師找我了。在見(jiàn)老師之前,我把錦明老師的怎么和老師交流的音頻課程,認(rèn)真的聽(tīng)了至少五六遍。覺(jué)得有點(diǎn)底氣...
    THY桃花顏閱讀 241評(píng)論 2 2

友情鏈接更多精彩內(nèi)容