天天看點

JNI學習筆記——局部和全局引用

 JNI将執行個體、數組類型暴露為不透明的引用。native代碼從不會直接檢查一個不透明的引用指針的上下文,而是通過使用JNI函數來通路由不透明的引用所指向的資料結構。因為隻處理不透明的引用,這樣就不需要擔心不同的java VM實作而導緻的不同的内部對象的布局。然而,還是有必要了解一下JNI中不同種類的引用:

1)JNI 支援3中不透明的引用:局部引用、全局引用和弱全局引用。

2)局部和全局引用,有着各自不同的生命周期。局部引用能夠被自動釋放,而全局引用和若全局引用在被程式員釋放之前,是一直有效的。

3)一個局部或者全局引用,使所提及的對象不能被垃圾回收。而弱全局引用,則允許提及的對象進行垃圾回收。

4)不是所有的引用都可以在所有上下文中使用的。例如:在一個建立傳回引用native方法之後,使用一個局部引用,這是非法的。

局部和全局引用

那麼到底什麼是局部引用,什麼事全局引用,它們有什麼不同?

局部引用

多數JNI函數都建立局部引用。例如JNI函數NewObject建立一個執行個體,并且傳回一個指向該執行個體的局部引用。

局部引用隻在建立它的native方法的動态上下文中有效,并且隻在native方法的一次調用中有效。所有局部引用隻在一個native方法的執行期間有效,在該方法傳回時,它就被回收。

在native方法中使用一個靜态變量來儲存一個局部引用,以便在随後的調用中使用該局部引用,這種方式是行不通的。例如以下例子,誤用了局部引用:

/* This code is illegal */
jstring
MyNewString(JNIEnv *env, jchar *chars, jint len)
{
    static jclass stringClass = NULL;
    jmethodID cid;
    jcharArray elemArr;
    jstring result;
    if (stringClass == NULL) {
        stringClass = (*env)->FindClass(env, "java/lang/String");
        if (stringClass == NULL) {
            return NULL; /* exception thrown */
        }
    }
    /* It is wrong to use the cached stringClass here,
       because it may be invalid. */
    cid = (*env)->GetMethodID(env, stringClass, "<init>", "([C)V");
    ...
    elemArr = (*env)->NewCharArray(env, len);
    ...
    result = (*env)->NewObject(env, stringClass, cid, elemArr);
    (*env)->DeleteLocalRef(env, elemArr);
    return result;
}
           

這種儲存局部引用的方式是不正确的,因為FindClass()傳回的是對java.lang.String的局部引用。這是因為,在native代碼從MyNewString傳回退出時,VM 會釋放所有局部引用,包括存儲在stringClass變量中的指向類對象的引用。這樣當再次後繼調用MyNewString時,可能會通路非法位址,導緻記憶體被破壞,或者系統崩潰。

局部引用失效,有兩種方式:‘ 1)系統會自動釋放局部變量。 2)程式員可以顯示地管理局部引用的生命周期,例如調用DeleteLocalRef。

一個局部引用可能在被摧毀之前,被傳給多個native方法。例如,MyNewString中,傳回一個由NewObject建立的字元串引用,它将由NewObject的調用者來決定是否釋放該引用。而在以下代碼中:

JNIEXPORT jstring JNICALL Java_C_f(JNIEnv *env, jobject this) {
      char *c_str = ...<pre class="cpp" name="code">      ... <pre class="cpp" name="code">return MyNewString(c_str);<pre class="cpp" name="code">}</pre>
<pre></pre>
<pre></pre>
<pre></pre>
<pre></pre>
<pre></pre>
<pre></pre>
</pre></pre>
           

在VM接收到來自Java_C_f的局部引用以後,将基礎字元串對象傳遞給ava_C_f的調用者,然後摧毀原本由MyNewString中調用的JNI函數NewObject所建立的局部引用。

局部對象隻屬于建立它們的線程,隻在該線程中有效。一個線程想要調用另一個線程建立的局部引用是不被允許的。将一個局部引用儲存到全局變量中,然後在其它線程中使用它,這是一種錯誤的程式設計。

全局引用

在一個native方法被多次調用之間,可以使用一個全局引用跨越它們。一個全局引用可以跨越多個線程,并且在被程式員釋放之前,一緻有效。和局部引用一樣,全局引用保證了所引用的對象不會被垃圾回收。

和局部引用不一樣(局部變量可以由多數JNI函數建立),全局引用隻能由一個JNI函數建立(NewGlobalRef)。下面是一個使用全局引用版本的MyNewString:

/* This code is OK */
jstring
MyNewString(JNIEnv *env, jchar *chars, jint len)
{
    static jclass stringClass = NULL;
    ...
    if (stringClass == NULL) {
        jclass localRefCls =
            (*env)->FindClass(env, "java/lang/String");
        if (localRefCls == NULL) {
            return NULL; /* exception thrown */
        }
        /* Create a global reference */
        stringClass = (*env)->NewGlobalRef(env, localRefCls);
        /* The local reference is no longer useful */
        (*env)->DeleteLocalRef(env, localRefCls);
        /* Is the global reference created successfully? */
        if (stringClass == NULL) {
            return NULL; /* out of memory exception thrown */
        }
    }
    ...
}
           

弱全局引用

弱全局引用是在java 2 SDK1.2才出現的。它由NewGolableWeakRef函數建立,并且被DeleteGloablWeakRef函數摧毀。和全局引用一樣,它可以跨native方法調用,也可以跨越不同線程。但是和全局引用不同的是,它不阻止對基礎對象的垃圾回收。下面是弱全局引用版的MyNewString:

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; /* can’t find class */
        }
        myCls2 = NewWeakGlobalRef(env, myCls2Local);
        if (myCls2 == NULL) {
            return; /* out of memory */
        }
    }
    ... /* use myCls2 */
}
           

弱全局引用在一個被native代碼緩存着的引用不想阻止基礎對象被垃圾回收時,非常有用。如以上例子,mypkg.MyCls.f需要緩存mypkg.MyCls2的引用。而通過将 mypkg.MyCls2緩存到弱引用中,能夠實作MyCls2類依舊可以被解除安裝。

上面代碼中,我們假設了MyCls類和MyCls2類的生命周期是相同的(例如,在同一個類中被加載、解除安裝)。是以沒有考慮MyCls2被解除安裝了,然後在類MyCls和native方法的實作Java_mypkg_MyCls_f還要被繼續使用時,再被重新加載起來的情況。針對于這個MyCls2類可能被解除安裝再加載的情況,在使用時,需要檢查該弱全局引用是否還有效。如何檢查,這将在下面提到。

比較引用

可以用JNI函數IsSameObject來檢查給定的兩個局部引用、全局引用或者弱全局引用,是否指向同一個對象。

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

傳回值為: JNI_TRUE,表示兩個對象一緻,是同一個對象。 JNI_FALSE,表示兩個對象不一緻,不是同一個對象。

在java VM中NULL是null的引用。 如果一個對象obj是局部引用或者全局引用,則可以這樣來檢查它是否指向null對象:

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

或者:

NULL == obj
           

而對于弱全局引用,以上規則需要改變一下:

我們可以用這個函數來判斷一個非0弱全局引用wobj所指向的對象是否仍舊存活着(依舊有效)。

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

傳回值: JNI_TRUE,表示對象已經被回收了。 JNI_FALSE,表示wobj指向的對象,依舊有效。

釋放引用

除了引用的對象要占用記憶體,每個JNI引用本身也會消耗一定記憶體。作為一個JNI程式員,應該對在一段給定的時間裡,程式會用到的引用的個數,做到心中有數。特别是,盡管程式所建立的局部引用最終會被VM會被自動地釋放,仍舊需要知道在程式在執行期間的任何時刻,建立的局部引用的上限個數。建立過多的引用,即便他們是瞬間、短暫的,也會導緻記憶體耗盡。

釋放局部引用

多數情況下,在執行一個native方法時,你不需要擔心局部引用的釋放,java VM會在native方法傳回調用者的時候釋放。然而有時候需要JNI程式員顯示的釋放局部引用,來避免過高的記憶體使用。那麼什麼時候需要顯示的釋放呢,且看一下情景: 1)在單個native方法調用中,建立了大量的局部引用。這可能會導緻JNI局部引用表溢出。此時有必要及時地删除那些不再被使用的局部引用。例如以下代碼,在該循環中,每次都有可能建立一個巨大的字元串數組。在每個疊代之後,native代碼需要顯示地釋放指向字元串元素的局部引用:

for (i = 0; i < len; i++) {
    jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
    ... /* process jstr */
    (*env)->DeleteLocalRef(env, jstr);
}
           

2)你可能要建立一個工具函數,它會被未知的上下文調用。例如之前提到到MyNewString這個例子,它在每次傳回調用者欠,都及時地将局部引用釋放。

3)native方法,可能不會傳回(例如,一個可能進入無限事件分發的循環中的方法)。此時在循環中釋放局部引用,是至關重要的,這樣才能不會無限期地累積,進而導緻記憶體洩露。

4)native方法可能通路一個巨大的對象,是以,建立了一個指向該對象的局部引用。native方法在傳回調用者之前,除通路對象之外,還執行了額外的計算。指向這個大對象的局部引用,将會包含該對象,以防被垃圾回收。這個現象會持續到native方法傳回到調用者時,即便這個對象不會再被使用,也依舊會受保護。在以下例子中,由于在lengthyComputation()前,顯示地調用了DeleteLocalRef,是以垃圾回收器有機會可以釋放lref所指向的對象。

/* A native method implementation */
JNIEXPORT void JNICALL
Java_pkg_Cls_func(JNIEnv *env, jobject this)
{
    lref = ...              /* a large Java object */
    ...                     /* last use of lref */
    (*env)->DeleteLocalRef(env, lref);
    lengthyComputation();   /* may take some time */
    return;                 /* all local refs are freed */
}
           

這個情形的實質,就是允許程式在native方法執行期間,java的垃圾回收機制有機會回收native代碼不在通路的對象。

在java 2 SDK1.2中管理局部引用

不知道java 7怎麼樣了,應該更強大吧,有時間,去看看,這裡且按照java2的特性來吧。

SDK1.2中提供了一組額外的函數來管理局部引用的生命周期。他們是EnsureLocalCapacity、NewLocalRef、PushLocalFram以及PopLocalFram。

JNI的規範要求VM可以自動確定每個native方法可以建立至少16個局部引用。經驗顯示,如果native方法中未包含和java VM的對象進行複雜的互相操作,這個容量對大多數native方法而言,已經足夠了。如果,出現這還不夠的情況,需要建立更多的局部引用,那麼native方法可以調用EnsureLocalCapacity來保證這些局部引用有足夠的空間。

/* The number of local references to be created is equal to
   the length of the array. */
if ((*env)->EnsureLocalCapacity(env, len)) < 0) {
    ... /* out of memory */
}
for (i = 0; i < len; i++) {
    jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
    ... /* process jstr */
    /* DeleteLocalRef is no longer necessary */
}
           

這樣做,所消耗的記憶體,自然就有可能比之前的版本來的多。

另外,PushLocalFram\PopLocalFram函數允許程式員建立嵌套作用域的局部引用。如下代碼:

#define N_REFS ... /* the maximum number of local references
  used in each iteration */
for (i = 0; i < len; i++) {
    if ((*env)->PushLocalFrame(env, N_REFS) < 0) {
        ... /* out of memory */
    }
    jstr = (*env)->GetObjectArrayElement(env, arr, i);
    ... /* process jstr */
    (*env)->PopLocalFrame(env, NULL);
}
           

PushLocalFram為指定數目的局部引用,建立一個新的作用域,PopLocalFram摧毀最上層的作用域,并且釋放該域中的所有局部引用。

使用這兩個函數的好處是它們可以管理局部引用的生命周期,而不需關系在執行過程中可能被建立的每個單獨局部引用。例子中,如果處理jstr的過程,建立了額外的局部引用,它們也會在PopLocalFram之後被立即釋放。

NewLocalRef函數,在你寫一個工具函數時,非常有用。這個會在下面章節——管理引用的規則,具體分析。

native代碼可能會建立超出16個局部引用的範圍,也可能将他們儲存在PushLocalFram或者EnsureLocalCapacity調用,VM會為局部引用配置設定所需要的記憶體。然而,這些記憶體是否足夠,是沒有保證的。如果記憶體配置設定失敗,虛拟機将會退出。

釋放全局引用

在native代碼不再需要通路一個全局引用的時候,應該調用DeleteGlobalRef來釋放它。如果調用這個函數失敗,Java VM将不會回收對應的對象。

在native代碼不在需要通路一個弱全局引用的時候,應該調用DeleteWeakGlobalRef來釋放它。如果調用這個函數失敗了,java VM 仍舊将會回收對應的底層對象,但是,不會回收這個弱引用本身所消耗掉的記憶體。

管理引用的規則

管理引用的目的是為了清除不需要的記憶體占用和對象保留。

總體來說,隻有兩種類型的native代碼:直接實作native方法的函數,在二進制上下文中被使用的工具函數。

在寫native方法的實作的時候,需要當心在循環中過度建立局部引用,以及在native方法中被建立的,卻不傳回給調用者的局部引用。在native方法方法傳回後還留有16個局部引用在使用中,将它們交給java VM來釋放,這是可以接受的。但是native方法的調用,不應該引起全局引用和弱全局引用的累積。應為這些引用不會在native方法返後被自動地釋放。

在寫工具函數的時候,必須要注意不能洩露任何局部引用或者超出該函數之外的執行。因為一個工具函數,可能在意料之外的上下文中,被不停的重複調用。任何不需要的引用建立都有可能導緻記憶體洩露。 1)當一個傳回一個基礎類型的工具函數被調用,它必須應該沒有局部引用、若全局引用的累積。 2)當一個傳回一個引用類型的工具函數被調用,它必須應該沒有局部、全局或若全局引用的累積,除了要被作為傳回值的引用。

一個工具函數以捕獲為目的建立一些全局或者弱全局引用,這是可接受的,因為隻有在最開始的時候,才會建立這些引用。

如果一個工具函數傳回一個引用,你應該使傳回的引用的類型(例如局部引用、全局引用)作為函數規範的一部分。它應該始終如一,而不是有時候傳回一個局部引用,有時候卻傳回一個全局引用。調用者需要知道工具函數傳回的引用的類型,以便正确地管理自己的JNI引用。以下代碼重複地調用一個工具工具函數(GetInfoString)。我們需要知道GetInfoString傳回的引用的類型,以便釋放該引用:

while (JNI_TRUE) {
    jstring infoString = GetInfoString(info);
    ... /* process infoString */
    ??? /* we need to call DeleteLocalRef, DeleteGlobalRef,
  or DeleteWeakGlobalRef depending on the type of
  reference returned by GetInfoString. */
}
           

在java2 SDK1.2中,NewLocalRef函數可以用來保證一個工具函數一直傳回一個局部引用。為了說明這個問題,我們對MyNewString做一些改動,它緩存了一個被頻繁請求的字元串(“CommonString”)到全局引用:

jstring
MyNewString(JNIEnv *env, jchar *chars, jint len)
{
    static jstring result;
    /* wstrncmp compares two Unicode strings */
    if (wstrncmp("CommonString", chars, len) == 0) {
        /* refers to the global ref caching "CommonString" */
        static jstring cachedString = NULL;
        if (cachedString == NULL) {
            /* create cachedString for the first time */
            jstring cachedStringLocal = ... ;
            /* cache the result in a global reference */
            cachedString =
                (*env)->NewGlobalRef(env, cachedStringLocal);
        }
        return (*env)->NewLocalRef(env, cachedString);
    }
    ... /* create the string as a local reference and store in
  result as a local reference */
    return result;
}
           

正常的流程傳回的時候局部引用。就像之前解釋的那樣,我們必須将緩存字元儲存到一個全局引用中,這樣就可以在多個線程中調用native方法時,都能通路它。

return (*env)->NewLocalRef(env, cachedString);
           

這條語句,建立了一個局部引用,它指向了緩存在全局引用的指向的統一對象。作為和調用者的約定的一部分,MyNewString總是傳回一個局部引用。

PushLocalFram、PopLocalFram函數用來管理局部引用的生命周期特别得友善。隻需要在native函數的入口調用PushLocalFram,在函數退出時調用PopLocalFram,局部變量就會被釋放。

jobject f(JNIEnv *env, ...)
{
    jobject result;
    if ((*env)->PushLocalFrame(env, 10) < 0) {
        /* frame not pushed, no PopLocalFrame needed */
        return NULL;
    }
    ...
    result = ...;
    if (...) {
        /* remember to pop local frame before return */
        result = (*env)->PopLocalFrame(env, result);
        return result;
    }
    ...
    result = (*env)->PopLocalFrame(env, result);
    /* normal return */
    return result;
}
           

PopLocalFram函數調用失敗時,可能會導緻未定義的行為,例如VM崩潰。

轉自: http://blog.csdn.net/ljeagle/article/details/6713504