Android JNI開發系列(十二) JNI局部引用、全局引用和弱全局引用 原 薦

JNI 局部引用、全局引用和弱全局引用

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

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

    jclass str = (*env)->FindClass(env, "java/lang/String");
    jcharArray charArray = (*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釋放掉。我們可能在函數中將局部引用存儲在靜態變量中緩存起來,供下次調用時使用。這種方式是不可取的,因爲函數返回後局部引用很有可能會馬上釋放掉,靜態變量中存儲了就是一個被釋放後的內存地址,成爲野指針,這種是不能通過判NULL來檢測的。附代碼:

    #include <jni.h>
    
    /*錯誤的局部引用*/
    JNIEXPORT jstring JNICALL Java_org_professor_jni_bean_Person_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 (NULL == cls_string) {
            cls_string = (*env)->FindClass(env, "java/lang/String");
            if (NULL == cls_string) {
                return NULL;
            }
        }
        // 緩存String的構造方法ID
        if (NULL == cid_string) {
            cid_string = (*env)->GetMethodID(env, cls_string, "<init>", "([C)V");
            if (NULL == cid_string) {
                return NULL;
            }
        }
    
        //省略額外的代碼.......
        elemArray = (*env)->NewCharArray(env, len);
        // ....
        j_str = (*env)->NewObject(env, cls_string, cid_string, elemArray);
        // 釋放局部引用
        (*env)->DeleteLocalRef(env, elemArray);
        return j_str;
    }
    
    • 分析

      上面代碼中,略去了無關代碼,假設當我們調用了該方法

      JNIEXPORT jstring JNICALL
      JAVA org_professor_jni_MainActivity_callNewString(JNIEnv *env, jobject obj){
          char *c_str = ...;
          jcharArray char_arr = ...;
      
          //...
          return Java_org_professor_jni_bean_Person_newString(env,obj,char_arr,c_str);  
      }
      
      

      該方法返回後,JVM會釋放在這個方法執行期間創建的所有局部引用,也包含對String的Class引用cls_string。當再次調用Java_org_professor_jni_bean_Person_newString時候,Java_org_professor_jni_bean_Person_newString所指向的內存空間已經被釋放,成爲了一個野指針,會因非法內存訪問造成Crash。

  • 釋放局部引用

    釋放一個局部引用有兩種方式,

    • 本地方法執行完自動釋放
    • 手動調用DeleteLocalRef釋放

    理所當然,JVM會在函數返回後會被自動釋放,爲啥還要手動去釋放?大部分情況下我們實現一個Native方法時不必擔心局部引用的釋放問題,函數在被調用完成後,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_org_professor_jni_bean_Person_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 (local_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;
}

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

弱全局引用

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

JNIEXPORT void JNICALL
Java_org_professor_jni_MainActivity_testWeakRef(JNIEnv *env, jobject self) {
    static jclass clazz = NULL;
    if (clazz == NULL) {
        jclass clazzLocal = (*env)->FindClass(env, "org/professor/jni/bean/Student");
        if (NULL == clazzLocal) {
            return;
        }
        clazz = (*env)->NewWeakGlobalRef(env, clazzLocal);
        if (NULL == clazz) {
            return; /* 內存溢出 */
        }
    }
    //省略代碼... 
    /* 使用Student的引用 */
}

我們假設當前類和Student有相同的生命週期(例如,他們可能被相同的類加載器加載),因爲弱引用的存在,我們不必擔心當前類和它所在的本地代碼在被使用時,Student這個類出現先被unload,後來又會preload的情況。當然,如果真的發生這種情況時(當前類和Student此時的生命週期不同),我們在使用弱引用時,必須先檢查緩存過的弱引用是指向活動的類對象,還是指向一個已經被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_TRUEg_obj_ref仍然指向一個活動對象,會返回JNI_FALSE

當我們的本地代碼不再需要一個弱全局引用時,也應該調用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_org_professor_jni_bean_Person_WeakRef(JNIEnv *env, jobject self, jcharArray j_char_arr,
                                           jint len) {
         static jstring result;
        /* 使用wstrncmp函數比較兩個Unicode字符串 */
        //省略部分代碼
        if (strncmp("CommonString", char_arr, 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 fun(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當中。

相關:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章