JNI內(nèi)存管理

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

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

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

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

如果需要的話,當我們需要將這個jstr返回給java層使用的時候又要怎么辦呢?不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;
}

其實jni為了解決這個問題,設計了三種引用類型:

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

局部引用

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

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

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

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

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

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

這就會引起local reference table overflow:

1.png

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

有些同學可能會說,怎么可能會有人真的直接就在循環(huán)里不斷創(chuàng)建對象呢看锉。其實這種溢出大多數(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);
}

作為一個安全的程序員,在對象不再使用的時候,立馬使用DeleteLocalRef去將其釋放是一個很好的習慣。

局部引用棧幀

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

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

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);
}

這個時候可以考慮使用局部引用棧幀:

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);
}

我們在方法開頭PushLocalFrame,結(jié)尾PopLocalFrame,這樣整個方法就在一個局部引用幀里面,而在PopLocalFrame就會將該幀里面創(chuàng)建的局部引用全部釋放。

有的同學可能會想到一種場景,如果需要將某個局部引用當初返回值返回怎么辦?用局部引用幀會不會造成野指針?

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

jobject PopLocalFrame(jobject result)

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

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ù),局部引用幀里面的引用也是會失效的,所以不能直接將它返回,而是需要用PopLocalFrame為它創(chuàng)建的新引用,這個引用才在父幀里面鸵熟。

多線程下的局部引用

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

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

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

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

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

我們在java層使用System.loadLibrary方法加載so的時候,c層的JNI_OnLoad方法會被調(diào)用,我們可以在拿到JavaVM指針并將它保存起來:

JavaVM* g_Vm;

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

之后可以在線程中使用它的AttachCurrentThread方法附加原生線程,然后在線程結(jié)束的時候使用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就失效了,同時我們用它創(chuàng)建的局部引用也會被回收流强。

全局引用

假設我們需要使用監(jiān)聽者模式在c層保存java對象的引用,并啟動線程執(zhí)行操作,在適當?shù)臅r候通知java層痹届。我們需要怎么做,一種<font color='red'>錯誤</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; // 錯誤的做法!!!
}

原因是這里傳進來的jobject其實也是局部引用,而局部引用是不能跨線程使用的。我們應該將它轉(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之后它才會失效打月。

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

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

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

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

弱全局引用

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

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

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

JNI中的NULL引用指向JVM中的null對象。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末奏篙,一起剝皮案震驚了整個濱河市柴淘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌秘通,老刑警劉巖为严,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異肺稀,居然都是意外死亡第股,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門话原,熙熙樓的掌柜王于貴愁眉苦臉地迎上來夕吻,“玉大人诲锹,你說我怎么就攤上這事∩嫦冢” “怎么了归园?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長稚矿。 經(jīng)常有香客問我蔓倍,道長,這世上最難降的妖魔是什么盐捷? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮默勾,結(jié)果婚禮上碉渡,老公的妹妹穿的比我還像新娘。我一直安慰自己母剥,他們只是感情好滞诺,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著环疼,像睡著了一般习霹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上炫隶,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天淋叶,我揣著相機與錄音,去河邊找鬼伪阶。 笑死煞檩,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的栅贴。 我是一名探鬼主播斟湃,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼檐薯!你這毒婦竟也來了凝赛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤坛缕,失蹤者是張志新(化名)和其女友劉穎墓猎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體祷膳,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡陶衅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了直晨。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片搀军。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡膨俐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出罩句,到底是詐尸還是另有隱情焚刺,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布门烂,位于F島的核電站乳愉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏屯远。R本人自食惡果不足惜蔓姚,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望慨丐。 院中可真熱鬧坡脐,春花似錦、人聲如沸房揭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽捅暴。三九已至恬砂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蓬痒,已是汗流浹背泻骤。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留乳幸,地道東北人瞪讼。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像粹断,于是被迫代替她去往敵國和親符欠。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容

  • *** 說明:本文不代表博主觀點瓶埋,均是由以下資料整理的讀書筆記希柿。 *** 【參考資料】 1、向您的Android ...
    莫緒旻_向嶼閱讀 1,143評論 0 5
  • JNI概念 JNI是本地語言編程接口养筒。它允許運行在JVM中的Java代碼和用C曾撤、C++或匯編寫的本地代碼相互操作。...
    加油碼農(nóng)閱讀 988評論 0 0
  • 0.要素1.類操作2.異常操作3.全局及局部引用4.對象操作5.字符串操作6.數(shù)組操作7.訪問對象的屬性和方法7....
    MagicalGuy閱讀 1,319評論 0 2
  • ref: Android Studio開發(fā)JNI示例Android NDK 開發(fā)(二)JNI 傳遞參數(shù)和返回值An...
    richy_閱讀 1,579評論 0 8
  • 今天兒子的老師找我了晕粪。在見老師之前挤悉,我把錦明老師的怎么和老師交流的音頻課程,認真的聽了至少五六遍巫湘。覺得有點底氣...
    THY桃花顏閱讀 180評論 2 2