面試的時(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)存有四種釋放方式:
- 程序員可以手動(dòng)調(diào)用DeleteLocalRef去釋放
- c層方法執(zhí)行完成返回java層的時(shí)候,jvm會(huì)遍歷局部引用表去釋放
- 使用PushLocalFrame/PopLocalFrame創(chuàng)建/銷(xiāo)毀局部引用棧幀的時(shí)候,在PopLocalFrame里會(huì)釋放幀內(nèi)創(chuàng)建的引用
- 如果使用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:

所以在使用完之后一定記得調(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ì)象。