天天看點

JNI/NDK開發指南(十)——JNI局部引用、全局引用和弱全局引用三種引用簡介及差別局部引用全局引用弱全局引用引用比較釋放全局引用管理引用的規則

轉載請注明出處:http://blog.csdn.net/xyang81/article/details/44657385

    這篇文章比較偏理論,詳細介紹了在編寫本地代碼時三種引用的使用場景和注意事項。可能看起來有點枯燥,但引用是在JNI中最容易出錯的一個點,如果使用不當,容易使程式造成記憶體溢出,程式崩潰等現象。是以講得比較細,有些地方看起來可能比較啰嗦,還請輕啪!《Android JNI局部引用表溢出:local reference table overflow (max=512)》這篇文章是一個JNI引用使用不當造成引用表溢出,最終導緻程式崩潰的例子。建議看完這篇文章之後,再去看。

    做Java的朋友都知道,在編碼的過程當中,記憶體管理這一塊完全是透明的。new一個類的執行個體時,隻知道建立完這個類的執行個體之後,會傳回這個執行個體的一個引用,然後就可以拿着這個引用通路它的所有資料成員了(屬性、方法)。完全不用管JVM内部是怎麼實作的,如何為新建立的對象來申請記憶體,也不用管對象使用完之後記憶體是怎麼釋放的,隻需知道有一個垃圾回器在幫忙管理這些事情就OK的了。有經驗的朋友也許知道啟動一個Java程式,如果沒有手動建立其它線程,預設會有兩個線程在跑,一個是main線程,另一個就是GC線程(負責将一些不再使用的對象回收)。如果你曾經是做Java的然後轉去做C++,會感覺很“蛋疼”,在C++中new一個對象,使用完了還要做一次delete操作,malloc一次同樣也要調用free來釋放相應的記憶體,否則你的程式就會有記憶體洩露了。而且在C/C++中記憶體還分棧空間和堆空間,其中局部變量、函數形參變量、for中定義的臨時變量所配置設定的記憶體空間都是存放在棧空間(而且還要注意大小的限制),用new和malloc申請的記憶體都存放在堆空間。。。但C/C++裡的記憶體管理還遠遠不止這些,這些隻是最基礎的記憶體管理常識。做Java的童鞋聽到這些肯定會偷樂了,咱寫Java的時候這些都不用管,全都交給GC就萬事無優了。手動管理記憶體雖然麻煩,而且需要特别細心,一不小心就有可能造成記憶體洩露和野指針通路等程式緻命的問題,但凡事都有利弊,手動申請和釋放記憶體對程式的掌握比較靈活,不會受到平台的限制。比如我們寫Android程式的時候,記憶體使用就受Dalivk虛拟機的限制,從最初版本的16~24M,到後來的32M到64M,可能随着以後移動裝置實體記憶體的不大擴大,後面的Android版本記憶體限制可能也會随着提高。但在C/C++這層,就完全不受虛拟機的限制了。比如要在Android中要存儲一張超高清的圖檔,剛好這張圖檔的大小超過了Dalivk虛拟機對每個應用的記憶體大小限制,Java此時就顯得無能為力了,但在C/C++看來就是小菜一碟了,malloc(1024*1024*50),要多少記憶體,您說個數。。。C/C++程式員得意的說道~~Java不是說是一門純面象對象的語言嗎,是以除了基本資料類型外,其它任何類型所建立的對象,JVM所申請的記憶體都存在堆空間。上面提高到了GC,是負責回收不再使用的對象,它的全稱是Garbage Collection,也就是所謂的垃圾回收。JVM會在适當的時機觸發GC操作,一旦進行GC操作,就會将一些不再使用的對象進行回收。那麼哪些對象會被認為是不再使用,并且可以被回收的呢?我們來看下面二張圖:(注:圖摘自部落客郭霖的《Android最佳性能實踐(二)——分析記憶體的使用情況》)

JNI/NDK開發指南(十)——JNI局部引用、全局引用和弱全局引用三種引用簡介及差別局部引用全局引用弱全局引用引用比較釋放全局引用管理引用的規則

上圖當中,每個藍色的圓圈就代表一個記憶體當中的對象,而圓圈之間的箭頭就是它們的引用關系。這些對象有些是處于活動狀态的,而有些就已經不再被使用了。那麼GC操作會從一個叫作Roots的對象開始檢查,所有它可以通路到的對象就說明還在使用當中,應該進行保留,而其它的對象就表示已經不再被使用了,如下圖所示:

JNI/NDK開發指南(十)——JNI局部引用、全局引用和弱全局引用三種引用簡介及差別局部引用全局引用弱全局引用引用比較釋放全局引用管理引用的規則

可以看到,目前所有黃色的對象都處于活動狀态,仍然會被系統繼續保留,而藍色的對象就會在GC操作當中被系統回收掉了,這就是JVM執行一次GC的簡單流程。

    上面說的廢話好像有點多哈,下面進入正題。通過上面的讨論,大家都知道,如果一個Java對象沒有被其它成員變量或靜态變量所引用的話,就随時有可能會被GC回收掉。是以我們在編寫本地代碼時,要注意從JVM中擷取到的引用在使用時被GC回收的可能性。由于本地代碼不能直接通過引用操作JVM内部的資料結構,要進行這些操作必須調用相應的JNI接口來間接操作所引用的資料結構。JNI提供了和Java相對應的引用類型,供本地代碼配合JNI接口間接操作JVM内部的資料内容使用。如:jobject、jstring、jclass、jarray、jintArray等。因為我們隻通過JNI接口操作JNI提供的引用類型資料結構,而且每個JVM都實作了JNI規範相應的接口,是以我們不必擔心特定JVM中對象的存儲方式和内部資料結構等資訊,我們隻需要學習JNI中三種不同的引用即可。

由于Java程式運作在虛拟機中的這個特點,在Java中建立的對象、定義的變量和方法,内部對象的資料結構是怎麼定義的,隻有JVM自己知道。如果我們在C/C++中想要通路Java中對象的屬性和方法時,是不能夠直接操作JVM内部Java對象的資料結構的。想要在C/C++中正确的通路Java的資料結構,JVM就必須有一套規則來限制C/C++與Java互相通路的機制,是以才有了JNI規範,JNI規範定義了一系列接口,任何實作了這套JNI接口的Java虛拟機,C/C++就可以通過調用這一系列接口來間接的通路Java中的資料結構。比如前面文章中學習到的常用JNI接口有:GetStringUTFChars(從Java虛拟機中擷取一個字元串)、ReleaseStringUTFChars(釋放從JVM中擷取字元串所配置設定的記憶體空間)、NewStringUTF、GetArrayLength、GetFieldID、GetMethodID、FindClass等。

三種引用簡介及差別

    在JNI規範中定義了三種引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。差別如下:

1、局部引用:通過NewLocalRef和各種JNI接口建立(FindClass、NewObject、GetObjectClass和NewCharArray等)。會阻止GC回收所引用的對象,不在本地函數中跨函數使用,不能跨線前使用。函數傳回後局部引用所引用的對象會被JVM自動釋放,或調用DeleteLocalRef釋放。

(*env)->DeleteLocalRef(env,local_ref)

jclass cls_string = (*env)->FindClass(env, "java/lang/String");
jcharArray charArr = (*env)->NewCharArray(env, len);
jstring str_obj = (*env)->NewObject(env, cls_string, cid_string, elemArray);
jstring str_obj_local_ref = (*env)->NewLocalRef(env,str_obj);   // 通過NewLocalRef函數建立
...
           

2、全局引用:調用NewGlobalRef基于局部引用建立,會阻GC回收所引用的對象。可以跨方法、跨線程使用。JVM不會自動釋放,必須調用DeleteGlobalRef手動釋放

(*env)->DeleteGlobalRef(env,g_cls_string);

static jclass g_cls_string;
void TestFunc(JNIEnv* env, jobject obj) {
    jclass cls_string = (*env)->FindClass(env, "java/lang/String");
    g_cls_string = (*env)->NewGlobalRef(env,cls_string);
}
           

3、 弱全局引用:調用NewWeakGlobalRef基于局部引用或全局引用建立,不會阻止GC回收所引用的對象,可以跨方法、跨線程使用。引用不會自動釋放,在JVM認為應該回收它的時候(比如記憶體緊張的時候)進行回收而被釋放。或調用DeleteWeakGlobalRef手動釋放。

(*env)->DeleteWeakGlobalRef(env,g_cls_string)

static jclass g_cls_string;
void TestFunc(JNIEnv* env, jobject obj) {
    jclass cls_string = (*env)->FindClass(env, "java/lang/String");
    g_cls_string = (*env)->NewWeakGlobalRef(env,cls_string);
}
           

局部引用

局部引用也稱本地引用,通常是在函數中建立并使用。會阻止GC回收所引用的對象。比如,調用NewObject接口建立一個新的對象執行個體并傳回一個對這個對象的局部引用。局部引用隻有在建立它的本地方法傳回前有效,本地方法傳回到Java層之後,如果Java層沒有對傳回的局部引用使用的話,局部引用就會被JVM自動釋放。你可能會為了提高程式的性能,在函數中将局部引用存儲在靜态變量中緩存起來,供下次調用時使用。這種方式是錯誤的,因為函數傳回後局部引很可能馬上就會被釋放掉,靜态變量中存儲的就是一個被釋放後的記憶體位址,成了一個野針對,下次再使用的時候就會造成非法位址的通路,使程式崩潰。請看下面一個例子,錯誤的緩存了String的Class引用:

/*錯誤的局部引用*/
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString
(JNIEnv *env, jobject obj, jcharArray j_char_arr, jint len)
{
    jcharArray elemArray;
    jchar *chars = NULL;
    jstring j_str = NULL;
    static jclass cls_string = NULL;
    static jmethodID cid_string = NULL;
    // 注意:錯誤的引用緩存
    if (cls_string == NULL) {
        cls_string = (*env)->FindClass(env, "java/lang/String");
        if (cls_string == NULL) {
            return NULL;
        }
    }
    // 緩存String的構造方法ID
    if (cid_string == NULL) {
        cid_string = (*env)->GetMethodID(env, cls_string, "<init>", "([C)V");
        if (cid_string == NULL) {
            return NULL;
        }
    }

   //省略額外的代碼.......
    elemArray = (*env)->NewCharArray(env, len);
    // ....
    j_str = (*env)->NewObject(env, cls_string, cid_string, elemArray);
    // 釋放局部引用
    (*env)->DeleteLocalRef(env, elemArray);
    return j_str;
}
           

上面代碼中,我們省略了和我們讨論無關的代碼。因為FindClass傳回一個對java.lang.String對象的局部引用,上面代碼中緩存cls_string做法是錯誤的。假設一個本地方法C.f調用了newString:

JNIEXPORT jstring JNICALL
 Java_C_f(JNIEnv *env, jobject this)
 {
     char *c_str = ...;
     ...
     return newString(c_str);
}
           
Java_com_study_jnilearn_AccessCache_newString 下面簡稱newString

C.f方法傳回後,JVM會釋放在這個方法執行期間建立的所有局部引用,也包含對String的Class引用cls_string。當再次調用newString時,newString所指向引用的記憶體空間已經被釋放,成為了一個野指針,再通路這個指針的引用時,會導緻因非法的記憶體通路造成程式崩潰。

...
... = C.f(); // 第一次調是OK的
... = C.f(); // 第二次調用時,通路的是一個無效的引用.
...
           

釋放局部引用

釋放一個局部引用有兩種方式,一個是本地方法執行完畢後JVM自動釋放,另外一個是自己調用DeleteLocalRef手動釋放。既然JVM會在函數傳回後會自動釋放所有局部引用,為什麼還需要手動釋放呢?大部分情況下,我們在實作一個本地方法時不必擔心局部引用的釋放問題,函數被調用完成後,JVM 會自動釋放函數中建立的所有局部引用。盡管如此,以下幾種情況下,為了避免記憶體溢出,我們應該手動釋放局部引用:

1、JNI會将建立的局部引用都存儲在一個局部引用表中,如果這個表超過了最大容量限制,就會造成局部引用表溢出,使程式崩潰。經測試,Android上的JNI局部引用表最大數量是512個。當我們在實作一個本地方法時,可能需要建立大量的局部引用,如果沒有及時釋放,就有可能導緻JNI局部引用表的溢出,是以,在不需要局部引用時就立即調用DeleteLocalRef手動删除。比如,在下面的代碼中,本地代碼周遊一個特别大的字元串數組,每周遊一個元素,都會建立一個局部引用,當對使用完這個元素的局部引用時,就應該馬上手動釋放它。

for (i = ; i < len; i++) {
     jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
     ... /* 使用jstr */
     (*env)->DeleteLocalRef(env, jstr); // 使用完成之後馬上釋放
}
           

2、在編寫JNI工具函數時,工具函數在程式當中是公用的,被誰調用你是不知道的。上面newString這個函數示範了怎麼樣在工具函數中使用完局部引用後,調用DeleteLocalRef删除。不這樣做的話,每次調用newString之後,都會遺留兩個引用占用空間(elemArray和cls_string,cls_string不用static緩存的情況下)。

3、如果你的本地函數不會傳回。比如一個接收消息的函數,裡面有一個死循環,用于等待别人發送消息過來

while(true) { if (有新的消息) { 處理之。。。。} else { 等待新的消息。。。}}

。如果在消息循環當中建立的引用你不顯示删除,很快将會造成JVM局部引用表溢出。

4、局部引用會阻止所引用的對象被GC回收。比如你寫的一個本地函數中剛開始需要通路一個大對象,是以一開始就建立了一個對這個對象的引用,但在函數傳回前會有一個大量的非常複雜的計算過程,而在這個計算過程當中是不需要前面建立的那個大對象的引用的。但是,在計算的過程當中,如果這個大對象的引用還沒有被釋放的話,會阻止GC回收這個對象,記憶體一直占用者,造成資源的浪費。是以這種情況下,在進行複雜計算之前就應該把引用給釋放了,以免不必要的資源浪費。

/* 假如這是一個本地方法實作 */
JNIEXPORT void JNICALL Java_pkg_Cls_func(JNIEnv *env, jobject this)
{
   lref = ...              /* lref引用的是一個大的Java對象 */
   ...                     /* 在這裡已經處理完業務邏輯後,這個對象已經使用完了 */
   (*env)->DeleteLocalRef(env, lref); /* 及時删除這個對這個大對象的引用,GC就可以對它回收,并釋放相應的資源*/
   lengthyComputation();   /* 在裡有個比較耗時的計算過程 */
   return;                 /* 計算完成之後,函數傳回之前所有引用都已經釋放 */
}
           

管理局部引用

JNI提供了一系列函數來管理局部引用的生命周期。這些函數包括:EnsureLocalCapacity、NewLocalRef、PushLocalFrame、PopLocalFrame、DeleteLocalRef。JNI規範指出,任何實作JNI規範的JVM,必須確定每個本地函數至少可以建立16個局部引用(可以了解為虛拟機預設支援建立16個局部引用)。實際經驗表明,這個數量已經滿足大多數不需要和JVM中内部對象有太多互動的本地方函數。如果需要建立更多的引用,可以通過調用EnsureLocalCapacity函數,確定在目前線程中建立指定數量的局部引用,如果建立成功則傳回0,否則建立失敗,并抛出OutOfMemoryError異常。EnsureLocalCapacity這個函數是1.2以上版本才提供的,為了向下相容,在編譯的時候,如果申請建立的局部引用超過了本地引用的最大容量,在運作時JVM會調用FatalError函數使程式強制退出。在開發過程當中,可以為JVM添加-verbose:jni參數,在編譯的時如果發現本地代碼在試圖申請過多的引用時,會列印警告資訊提示我們要注意。在下面的代碼中,周遊數組時會擷取每個元素的引用,使用完了之後不手動删除,不考慮記憶體因素的情況下,它可以為這種建立大量的局部引用提供足夠的空間。由于沒有及時删除局部引用,是以在函數執行期間,會消耗更多的記憶體。

/*處理函數邏輯時,確定函數能建立len個局部引用*/
if((*env)->EnsureLocalCapacity(env,len) != ) {
    ... /*申請len個局部引用的記憶體空間失敗 OutOfMemoryError*/
    return;
}
for(i=; i < len; i++) {
    jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
    // ... 使用jstr字元串
    /*這裡沒有删除在for中臨時建立的局部引用*/
}
           

另外,除了EnsureLocalCapacity函數可以擴充指定容量的局部引用數量外,我們也可以利用Push/PopLocalFrame函數對建立作用範圍層層嵌套的局部引用。例如,我們把上面那段處理字元串數組的代碼用Push/PopLocalFrame函數對重寫:

#define N_REFS ... /*最大局部引用數量*/
for (i = ; i < len; i++) {
    if ((*env)->PushLocalFrame(env, N_REFS) != ) {
        ... /*記憶體溢出*/
    }
     jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
     ... /* 使用jstr */
     (*env)->PopLocalFrame(env, NULL);
}
           

PushLocalFrame為目前函數中需要用到的局部引用建立了一個引用堆棧,(如果之前調用PushLocalFrame已經建立了Frame,在目前的本地引用棧中仍然是有效的)每周遊一次調用

(*env)->GetObjectArrayElement(env, arr, i);

傳回一個局部引用時,JVM會自動将該引用壓入目前局部引用棧中。而PopLocalFrame負責銷毀棧中所有的引用。這樣一來,Push/PopLocalFrame函數對提供了對局部引用生命周期更友善的管理,而不需要時刻關注擷取一個引用後,再調用DeleteLocalRef來釋放引用。在上面的例子中,如果在處理jstr的過程當中又建立了局部引用,則PopLocalFrame執行時,這些局部引用将全都會被銷毀。在調用PopLocalFrame銷毀目前frame中的所有引用前,如果第二個參數result不為空,會由result生成一個新的局部引用,再把這個新生成的局部引用存儲在上一個frame中。請看下面的示例:

// 函數原型
jobject (JNICALL *PopLocalFrame)(JNIEnv *env, jobject result);

jstring other_jstr;
for (i = ; i < len; i++) {
    if ((*env)->PushLocalFrame(env, N_REFS) != ) {
        ... /*記憶體溢出*/
    }
     jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
     ... /* 使用jstr */
     if (i == ) {
        other_jstr = jstr;
     }
    other_jstr = (*env)->PopLocalFrame(env, other_jstr);  // 銷毀局部引用棧前傳回指定的引用
}
           

還要注意的一個問題是,局部引用不能跨線程使用,隻在建立它的線程有效。不要試圖在一個線程中建立局部引用并存儲到全局引用中,然後在另外一個線程中使用。

全局引用

全局引用可以跨方法、跨線程使用,直到它被手動釋放才會失效。同局部引用一樣,也會阻止它所引用的對象被GC回收。與局部引用建立方式不同的是,隻能通過NewGlobalRef函數建立。下面這個版本的newString示範怎麼樣使用一個全局引用:

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString
(JNIEnv *env, jobject obj, jcharArray j_char_arr, jint len)
{
    // ...
    jstring jstr = NULL;
    static jclass cls_string = NULL;
    if (cls_string == NULL) {
        jclass local_cls_string = (*env)->FindClass(env, "java/lang/String");
        if (cls_string == NULL) {
            return NULL;
        }

        // 将java.lang.String類的Class引用緩存到全局引用當中
        cls_string = (*env)->NewGlobalRef(env, local_cls_string);

        // 删除局部引用
        (*env)->DeleteLocalRef(env, local_cls_string);

        // 再次驗證全局引用是否建立成功
        if (cls_string == NULL) {
            return NULL;
        }
    }

    // ....
    return jstr;
}
           

弱全局引用

弱全局引用使用

NewGlobalWeakRef

建立,使用

DeleteGlobalWeakRef

釋放。下面簡稱弱引用。與全局引用類似,弱引用可以跨方法、線程使用。但與全局引用很重要不同的一點是,弱引用不會阻止GC回收它引用的對象。在newString這個函數中,我們也可以使用弱引用來存儲String的Class引用,因為java.lang.String這個類是系統類,永遠不會被GC回收。當本地代碼中緩存的引用不一定要阻止GC回收它所指向的對象時,弱引用就是一個最好的選擇。假設,一個本地方法mypkg.MyCls.f需要緩存一個指向類mypkg.MyCls2的引用,如果在弱引用中緩存的話,仍然允許mypkg.MyCls2這個類被unload,因為弱引用不會阻止GC回收所引用的對象。請看下面的代碼段:

JNIEXPORT void JNICALL
Java_mypkg_MyCls_f(JNIEnv *env, jobject self)
{
    static jclass myCls2 = NULL;
    if (myCls2 == NULL)
    {
        jclass myCls2Local = (*env)->FindClass(env, "mypkg/MyCls2");
        if (myCls2Local == NULL)
        {
            return; /* 沒有找到mypkg/MyCls2這個類 */
        }
        myCls2 = NewWeakGlobalRef(env, myCls2Local);
        if (myCls2 == NULL)
        {
            return; /* 記憶體溢出 */
        }
    }
    ... /* 使用myCls2的引用 */
}
           

我們假設MyCls和MyCls2有相同的生命周期(例如,他們可能被相同的類加載器加載),因為弱引用的存在,我們不必擔心MyCls和它所在的本地代碼在被使用時,MyCls2這個類出現先被unload,後來又會preload的情況。當然,如果真的發生這種情況時(MyCls和MyCls2此時的生命周期不同),我們在使用弱引用時,必須先檢查緩存過的弱引用是指向活動的類對象,還是指向一個已經被GC給unload的類對象。下面馬上告訴你怎樣檢查弱引用是否活動,即引用的比較。

引用比較

給定兩個引用(不管是全局、局部還是弱全局引用),我們隻需要調用IsSameObject來判斷它們兩個是否指向相同的對象。例如:

(*env)->IsSameObject(env, obj1, obj2)

,如果obj1和obj2指向相同的對象,則傳回JNI_TRUE(或者1),否則傳回JNI_FALSE(或者0)。有一個特殊的引用需要注意:NULL,JNI中的NULL引用指向JVM中的null對象。如果obj是一個局部或全局引用,使用

(*env)->IsSameObject(env, obj, NULL)

或者 obj == NULL 來判斷obj是否指向一個null對象即可。但需要注意的是,IsSameObject用于弱全局引用與NULL比較時,傳回值的意義是不同于局部引用和全局引用的:

jobject local_obj_ref = (*env)->NewObject(env, xxx_cls,xxx_mid);
jobject g_obj_ref = (*env)->NewWeakGlobalRef(env, local_ref);
// ... 業務邏輯處理
jboolean isEqual = (*env)->IsSameObject(env, g_obj_ref, NULL);
           

在上面的IsSameObject調用中,如果g_obj_ref指向的引用已經被回收,會傳回JNI_TRUE,如果wobj仍然指向一個活動對象,會傳回JNI_FALSE。

釋放全局引用

每一個JNI引用被建立時,除了它所指向的JVM中對象的引用需要占用一定的記憶體空間外,引用本身也會消耗掉一個數量的記憶體空間。作為一個優秀的程式員,我們應該對程式在一個給定的時間段内使用的引用數量要十分小心。短時間内建立大量而沒有被立即回收的引用很可能就會導緻記憶體溢出。

    當我們的本地代碼不再需要一個全局引用時,應該馬上調用DeleteGlobalRef來釋放它。如果不手動調用這個函數,即使這個對象已經沒用了,JVM也不會回收這個全局引用所指向的對象。

    同樣,當我們的本地代碼不再需要一個弱全局引用時,也應該調用DeleteWeakGlobalRef來釋放它,如果不手動調用這個函數來釋放所指向的對象,JVM仍會回收弱引用所指向的對象,但弱引用本身在引用表中所占的記憶體永遠也不會被回收。

管理引用的規則

前面對三種引用已做了一個全面的介紹,下面來總結一下引用的管理規則和使用時的一些注意事項,使用好引用的目的就是為了減少記憶體使用和對象被引用保持而不能釋放,造成記憶體浪費。是以在開發當中要特别小心!

通常情況下,有兩種本地代碼使用引用時要注意:

1、 直接實作Java層聲明的native函數的本地代碼

當編寫這類本地代碼時,要當心不要造成全局引用和弱引用的累加,因為本地方法執行完畢後,這兩種引用不會被自動釋放。

2、被用在任何環境下的工具函數。例如:方法調用、屬性通路和異常處理的工具函數等。

編寫工具函數的本地代碼時,要當心不要在函數的調用軌迹上遺漏任何的局部引用,因為工具函數被調用的場合和次數是不确定的,一量被大量調用,就很有可能造成記憶體溢出。是以在編寫工具函數時,請遵守下面的規則:

1> 一個傳回值為基本類型的工具函數被調用時,它決不能造成局部、全局、弱全局引用被回收的累加

2> 當一個傳回值為引用類型的工具函數被調用時,它除了傳回的引用以外,它決不能造成其它局部、全局、弱引用的累加

對于工具函數來說,為了使用緩存技術而建立一些全局引用或者弱全局引用是正常的。如果一個工具函數傳回的是一個引用,我們應該寫好注釋詳細說明傳回引用的類型,以便于使用者更好的管理它們。下面的代碼中,頻繁地調用工具函數GetInfoString,我們需要知道GetInfoString傳回引用的類型是什麼,以便于每次使用完成後調用相應的JNI函數來釋放掉它。

while (JNI_TRUE) {
     jstring infoString = GetInfoString(info);
     ... /* 處理infoString */
     ??? /* 使用完成之後,調用DeleteLocalRef、DeleteGlobalRef、DeleteWeakGlobalRef哪一個函數來釋放這個引用呢?*/
}
           

函數NewLocalRef有時被用來確定一個工具函數傳回一個局部引用。我們改造一下newString這個函數,示範一下這個函數的用法。下面的newString是把一個被頻繁調用的字元串“CommonString”緩存在了全局引用裡:

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString
{
    static jstring result;
    /* 使用wstrncmp函數比較兩個Unicode字元串 */
    if (wstrncmp("CommonString", chars, len) == )
    {
        /* 将"CommonString"這個字元串緩存到全局引用中 */
        static jstring cachedString = NULL;
        if (cachedString == NULL)
        {
            /* 先建立"CommonString"這個字元串 */
            jstring cachedStringLocal = ...;
            /* 然後将這個字元串緩存到全局引用中 */
            cachedString = (*env)->NewGlobalRef(env, cachedStringLocal);
        }
        // 基于全局引用建立一個局引用傳回,也同樣會阻止GC回收所引用的這個對象,因為它們指向的是同一個對象
        return (*env)->NewLocalRef(env, cachedString);  
    }
    ... 
    return result;
}
           

在管理局部引用的生命周期中,Push/PopLocalFrame是非常友善且安全的。我們可以在本地函數的入口處調用PushLocalFrame,然後在出口處調用PopLocalFrame,這樣的話,在函數内任何位置建立的局部引用都會被釋放。而且,這兩個函數是非常高效的,強烈建議使用它們。需要注意的是,如果在函數的入口處調用了PushLocalFrame,記住要在函數所有出口(有return語句出現的地方)都要調用PopLocalFrame。在下面的代碼中,對PushLocalFrame的調用隻有一次,但調用PopLocalFrame确有多次,當然你也可以使用goto語句來統一處理。

jobject f(JNIEnv *env, ...)
{
    jobject result;
    if ((*env)->PushLocalFrame(env, ) < )
    {
        /* 調用PushLocalFrame擷取個局部引用失敗,不需要調用PopLocalFrame */
        return NULL;
    }
    ...
    result = ...; // 建立局部引用result
    if (...)
    {
        /* 傳回前先彈出棧頂的frame */
        result = (*env)->PopLocalFrame(env, result);
        return result;
    }
    ...
    result = (*env)->PopLocalFrame(env, result);
    /* 正常傳回 */
    return result;
}
           

上面的代碼同樣示範了函數PopLocalFrame的第二個參數的用法,局部引用result一開始在PushLocalFrame建立在目前frame裡面,而把result傳入PopLocalFrame中時,PopLocalFrame在彈出目前的frame前,會由result生成一個新的局部引用,再将這個新生成的局部引用存儲在上一個frame當中。

繼續閱讀