NDK開發-JNI 局部引用、全局引用和弱全局引用

看到一篇寫的很不錯的介紹JNI各類引用的文章 mark 了。java

  這篇文章比較偏理論,詳細介紹了在編寫本地代碼時三種引用的使用場景和注意事項。可能看起來有點枯燥,但引用是在 JNI 中最容易出錯的一個點,若是使用不當,容易使程序形成內存溢出,程序崩潰等現象。《Android JNI局部引用表溢出》這篇文章是一個 JNI 引用使用不當形成引用表溢出,最終致使程序崩潰的例子。建議看完這篇文章以後,再去看。c++

  作 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(1024102450)。C/C++ 程序員得意的說道,Java 不是說是一門純面象對象的語言嗎,因此除了基本數據類型外,其它任何類型所建立的對象,JVM 所申請的內存都存在堆空間。上面提升到了 GC,是負責回收再也不使用的對象,它的全稱是 Garbage Collection,也就是所謂的垃圾回收。JVM 會在適當的時機觸發 GC 操做,一旦進行 GC 操做,就會將一些再也不使用的對象進行回收。那麼哪些對象會被認爲是再也不使用,而且能夠被回收的呢?咱們來看下面二張圖。(注:圖摘自博主郭霖的《Android 最佳性能實踐(二)——分析內存的使用狀況》)git

http://gnaix92.github.io/blog_images/ndk/12.png

  上圖當中,每一個藍色的圓圈就表明一個內存當中的對象,而圓圈之間的箭頭就是它們的引用關係。這些對象有些是處於活動狀態的,而有些就已經再也不被使用了。那麼 GC 操做會從一個叫做 Roots 的對象開始檢查,全部它能夠訪問到的對象就說明還在使用當中,應該進行保留,而其它的對象就表示已經再也不被使用了,以下圖所示:程序員

http://gnaix92.github.io/blog_images/ndk/13.png

  能夠看到,目前全部黃色的對象都處於活動狀態,仍然會被系統繼續保留,而藍色的對象就會在 GC 操做當中被系統回收掉了,這就是 JVM 執行一次 GC 的簡單流程。github

  上面說的廢話好像有點多哈,下面進入正題。經過上面的討論,你們都知道,若是一個 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)。區別以下:安全

局部引用

  經過 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函數建立
...

全局引用

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

弱全局引用

  調用 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 會自動釋放函數中建立的全部局部引用。儘管如此,如下幾種狀況下,爲了不內存溢出,咱們應該手動釋放局部引用。

  JNI 會將建立的局部引用都存儲在一個局部引用表中,若是這個表超過了最大容量限制,就會形成局部引用表溢出,使程序崩潰。經測試,Android 上的 JNI 局部引用表最大數量是 512 個。當咱們在實現一個本地方法時,可能須要建立大量的局部引用,若是沒有及時釋放,就有可能致使 JNI 局部引用表的溢出,因此,在不須要局部引用時就當即調用 DeleteLocalRef 手動刪除。好比,在下面的代碼中,本地代碼遍歷一個特別大的字符串數組,每遍歷一個元素,都會建立一個局部引用,當對使用完這個元素的局部引用時,就應該立刻手動釋放它。

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

  在編寫 JNI 工具函數時,工具函數在程序當中是公用的,被誰調用你是不知道的。上面 newString 這個函數演示了怎麼樣在工具函數中使用完局部引用後,調用 DeleteLocalRef 刪除。不這樣作的話,每次調用 newString 以後,都會遺留兩個引用佔用空間(elemArray和cls_string,cls_string 不用 static 緩存的狀況下)。

  若是你的本地函數不會返回。好比一個接收消息的函數,裏面有一個死循環,用於等待別人發送消息過來while(true) { if (有新的消息) { 處理之。。。。} else { 等待新的消息。。。}}。若是在消息循環當中建立的引用你不顯示刪除,很快將會形成 JVM 局部引用表溢出。

  局部引用會阻止所引用的對象被 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) != 0) {
    ... /*申請len個局部引用的內存空間失敗 OutOfMemoryError*/
    return;
}
for(i=0; i < len; i++) {
    jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
    // ... 使用jstr字符串
    /*這裏沒有刪除在for中臨時建立的局部引用*/
}

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

#define N_REFS ... /*最大局部引用數量*/
for (i = 0; i < len; i++) {
    if ((*env)->PushLocalFrame(env, N_REFS) != 0) {
        ... /*內存溢出*/
    }
     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 = 0; i < len; i++) {
    if ((*env)->PushLocalFrame(env, N_REFS) != 0) {
        ... /*內存溢出*/
    }
     jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
     ... /* 使用jstr */
     if (i == 2) {
        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 仍會回收弱引用所指向的對象,但弱引用自己在引用表中所佔的內存永遠也不會被回收。

管理引用的規則

  前面對三種引用已作了一個全面的介紹,下面來總結一下引用的管理規則和使用時的一些注意事項,使用好引用的目的就是爲了減小內存使用和對象被引用保持而不能釋放,形成內存浪費。因此在開發當中要特別當心!

  一般狀況下,有兩種本地代碼使用引用時要注意:

  直接實現Java層聲明的native函數的本地代碼 當編寫這類本地代碼時,要小心不要形成全局引用和弱引用的累加,由於本地方法執行完畢後,這兩種引用不會被自動釋放。
被用在任何環境下的工具函數。例如:方法調用、屬性訪問和異常處理的工具函數等。
編寫工具函數的本地代碼時,要小心不要在函數的調用軌跡上遺漏任何的局部引用,由於工具函數被調用的場合和次數是不肯定的,一量被大量調用,就頗有可能形成內存溢出。因此在編寫工具函數時,請遵照下面的規則:

  一個返回值爲基本類型的工具函數被調用時,它決不能形成局部、全局、弱全局引用被回收的累加。
  當一個返回值爲引用類型的工具函數被調用時,它除了返回的引用之外,它決不能形成其它局部、全局、弱引用的累加。
  對於工具函數來講,爲了使用緩存技術而建立一些全局引用或者弱全局引用是正常的。若是一個工具函數返回的是一個引用,咱們應該寫好註釋詳細說明返回引用的類型,以便於使用者更好的管理它們。下面的代碼中,頻繁地調用工具函數 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) == 0)
    {
        /* 將"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, 10) < 0)
    {
        /* 調用PushLocalFrame獲取10個局部引用失敗,不須要調用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 當中。

相關文章
相關標籤/搜索