JNI,Java Native Interface,是 native code 的編程介面。JNI 使 Java 代碼程式可以與 native code 互動——在 Java 程式中調用 native code;在 native code 中嵌入 JAVA 虛擬機器調用 Java 的代碼。
JNI 編程在軟體開發中運用廣泛,其優勢可以歸結為以下幾點:
利用 native code 的平台相關性,在平台相關的編程中彰顯優勢。
對 native code 的代碼重用。
native code 底層操作,更加高效。
然而任何事物都具有兩面性,JNI 編程也同樣如此。程式員在使用 JNI 時應當認識到 JNI 編程中如下的幾點弊端,揚長避短,才可以寫出更加完善、高效能的代碼:
從 Java 環境到 native code 的環境切換耗時、低效。
JNI 編程,如果操作不當,可能引起 JAVA 虛擬機器的崩潰。
JNI 編程,如果操作不當,可能引起記憶體流失。
JAVA 中的記憶體流失
JAVA 編程中的記憶體流失,從泄漏的記憶體位置角度可以分為兩種:JVM 中 Java Heap 的記憶體流失;JVM 記憶體中 native memory 的記憶體流失。
局部和全域引用
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 name="code" class="cpp"> ... <pre name="code" class="cpp">return MyNewString(c_str);<pre name="code" class="cpp">}
在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)
或者:
而對於弱全域引用,以上規則需要改變一下:
我們可以用這個函數來判斷一個非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 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崩潰。
記憶體流失問題
Java Heap 的記憶體流失
Java Object Storage Service在 JVM 進程空間中的 Java Heap 中,Java Heap 可以在 JVM 運行過程中動態變化。如果 Java 對象越來越多,佔據 Java Heap 的空間也越來越大,JVM 會在運行時擴充 Java Heap 的容量。如果 Java Heap 容量擴充到上限,並且在 GC 後仍然沒有足夠空間分配新的 Java 對象,便會拋出 out of memory 異常,導致 JVM 進程崩潰。
Java Heap 中 out of memory 異常的出現有兩種原因——①程式過於龐大,致使過多 Java 對象的同時存在;②程式編寫的錯誤導致 Java Heap 記憶體流失。
多種原因可能導致 Java Heap 記憶體流失。JNI 編程錯誤也可能導致 Java Heap 的記憶體流失。
JVM 中 native memory 的記憶體流失
從作業系統角度看,JVM 在運行時和其它進程沒有本質區別。在系統層級上,它們具有同樣的調度機制,同樣的記憶體配置方式,同樣的記憶體格局。
JVM 進程空間中,Java Heap 以外的記憶體空間稱為 JVM 的 native memory。進程的很多資源都是儲存在 JVM 的 native memory 中,例如載入的代碼映像,線程的堆棧,線程的管理控制塊,JVM 的待用資料、全域資料等等。也包括 JNI 程式中 native code 分配到的資源。
在 JVM 運行中,多數進程資源從 native memory 中動態分配。當越來越多的資源在 native memory 中分配,佔據越來越多 native memory 空間並且達到 native memory 上限時,JVM 會拋出異常,使 JVM 進程異常退出。而此時 Java Heap 往往還沒有達到上限。
多種原因可能導致 JVM 的 native memory 記憶體流失。例如 JVM 在運行中過多的線程被建立,並且在同時運行。JVM 為線程分配的資源就可能耗盡 native memory 的容量。
JNI 編程錯誤也可能導致 native memory 的記憶體流失。對這個話題的討論是本文的重點。
JNI 編程實現了 native code 和 Java 程式的互動,因此 JNI 代碼編程既遵循 native code 程式設計語言的編程規則,同時也遵守 JNI 編程的文檔規範。在記憶體管理方面,native code 程式設計語言本身的記憶體管理機制依然要遵循,同時也要考慮 JNI 編程的記憶體管理。
本章簡單概括 JNI 編程中顯而易見的記憶體流失。從 native code 程式設計語言自身的記憶體管理,和 JNI 規範附加的記憶體管理兩方面進行闡述。
Native Code 本身的記憶體流失
JNI 編程首先是一門具體的程式設計語言,或者 C 語言,或者 C++,或者彙編,或者其它 native 的程式設計語言。每門程式設計語言環境都實現了自身的記憶體管理機制。因此,JNI 程式開發人員要遵循 native 語言本身的記憶體管理機制,避免造成記憶體流失。以 C 語言為例,當用 malloc() 在進程堆中動態分配記憶體時,JNI 程式在使用完後,應當調用 free() 將記憶體釋放。總之,所有在 native 語言編程中應當注意的記憶體流失規則,在 JNI 編程中依然適應。
Native 語言本身引入的記憶體流失會造成 native memory 的記憶體,嚴重情況下會造成 native memory 的 out of memory。
Global Reference 引入的記憶體流失
JNI 編程還要同時遵循 JNI 的規範標準,JVM 附加了 JNI 編程特有的記憶體管理機制。
JNI 中的 Local Reference 只在 native method 執行時存在,當 native method 執行完後自動失效。這種自動失效,使得對 Local Reference 的使用相對簡單,native method 執行完後,它們所引用的 Java 對象的 reference count 會相應減 1。不會造成 Java Heap 中 Java 對象的記憶體流失。
而 Global Reference 對 Java 對象的引用一直有效,因此它們引用的 Java 對象會一直存在 Java Heap 中。程式員在使用 Global Reference 時,需要仔細維護對 Global Reference 的使用。如果一定要使用 Global Reference,務必確保在不用的時候刪除。就像在 C 語言中,調用 malloc() 動態分配一塊記憶體之後,調用 free() 釋放一樣。否則,Global Reference 引用的 Java 對象將永遠停留在 Java Heap 中,造成 Java Heap 的記憶體流失。