JNI提供了一些實例和數(shù)組類型(jobject诫肠、jclass捍岳、jstring紧帕、jarray等)作為不透明的引用供本地代碼使用。本地代碼永遠不會直接操作引用指向的VM內部的數(shù)據內容愉烙。要進行這些操作,必須通過使用JNI操作一個不引用來間接操作數(shù)據內容解取。因為只操作引用步责,你不必擔心特定JVM中對象的存儲方式等信息。這樣的話,你有必要了解一下JNI中的幾種不同的引用:
1蔓肯、 JNI支持三種引用:局部引用遂鹊、全局引用、弱全局引用(下文簡稱“弱引用”)秉扑。
2调限、 局部引用和全局引用有不同的生命周期。當本地方法返回時秦躯,局部引用會被自動釋放裆装。而全局引用和弱引用必須手動釋放。
3哨免、 局部引用或者全局引用會阻止GC回收它們所引用的對象琢唾,而弱引用則不會。
4身辨、 不是所有的引用可以被用在所有的場合芍碧。例如,一個本地方法創(chuàng)建一個局部引用并返回后泌豆,再對這個局部引用進行訪問是非法的踪危。
本章中,我們會詳細地討論這些問題贞远。合理地管理JNI引用是寫出高質量的代碼的基礎。
5.1 局部引用和全局引用
什么是全局引用和局部引用俱病?它們有什么不同?我們下面使用一些例子來說明途凫。
5.1.1 局部引用
大多數(shù)JNI函數(shù)會創(chuàng)建局部引用溢吻。例如,NewObject創(chuàng)建一個新的對象實例并返回一個對這個對象的局部引用犀盟。
局部引用只有在創(chuàng)建它的本地方法返回前有效硼砰。本地方法返回后,局部引用會被自動釋放恶阴。
你不能在本地方法中把局部引用存儲在靜態(tài)變量中緩存起來供下一次調用時使用豹障。下面的例子是MyNewString函數(shù)的一個修改版本,這里面使用局部引用的方法是錯誤的:
/* 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對象的局部引用垦写,上面的代碼中緩存stringClassr做法是錯誤的。假設一個本地方法C.f調用了MyNewString:
JNIEXPORT jstring JNICALL
Java_C_f(JNIEnv *env, jobject this)
{
char c_str = ...;
...
return MyNewString(c_str);
}
C.f方法返回后梯投,VM釋放了在這個方法執(zhí)行期間創(chuàng)建的所有局部引用分蓖,也包含對String類的引用stringClass。當再次調用MyNewString時么鹤,會試圖訪問一個無效的局部引用午磁,從而導致非法的內存訪問甚至系統(tǒng)崩潰毡们。
釋放一個局部引用有兩種方式昧辽,一個是本地方法執(zhí)行完畢后VM自動釋放登颓,另外一個是程序員通過DeleteLocalRef手動釋放。
既然VM會自動釋放局部引用咕痛,為什么還需要手動釋放呢喇嘱?因為局部引用會阻止它所引用的對象被GC回收。
局部引用只在創(chuàng)建它們的線程中有效者铜,跨線程使用是被禁止的。不要在一個線程中創(chuàng)建局部引用并存儲到全局引用中愉粤,然后到另外一個線程去使用拿撩。
5.1.2 全局引用
全局引用可以跨方法、跨線程使用影暴,直到它被手動釋放才會失效探赫。同局部引用一樣期吓,全局引用也會阻止它所引用的對象被GC回收。
與局部引用可以被大多數(shù)JNI函數(shù)創(chuàng)建不同讨勤,全局引用只能使用一個JNI函數(shù)創(chuàng)建: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 */
}
}
...
}
上面這段代碼中,一個由FindClass返回的局部引用被傳入NewGlobalRef屉来,用來創(chuàng)建一個對String類的全局引用。刪除localRefCls后茂契,我們檢查NewGlobalRef是否成功創(chuàng)建stringClass慨绳。
5.1.3 弱引用
弱引用使用NewGlobalWeakRef創(chuàng)建脐雪,使用DeleteGlobalWeakRef釋放。與全局引用類似战秋,弱引用可以跨方法脂信、線程使用。與全局引用不同的是梢薪,弱引用不會阻止GC回收它所指向的VM內部的對象尝哆。
在MyNewString中,我們也可以使用弱引用來存儲stringClass這個類引用琐馆,因為java.lang.String這個類是系統(tǒng)類恒序,永遠不會被GC回收歧胁。
當本地代碼中緩存的引用不一定要阻止GC回收它所指向的對象時,弱引用就是一個最好的選擇屠缭。假設崭参,一個本地方法mypkg.MyCls.f需要緩存一個指向類mypkg.MyCls2的引用,如果在弱引用中緩存的話奄喂,仍然允許mypkg.MyCls2這個類被unload:
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 /
}
我們假設MyCls和MyCls2有相同的生命周期(例如跨新,他們可能被相同的類加載器加載),因為弱引用的存在蟹肘,我們不必擔心MyCls和它所在的本地代碼在被使用時,MyCls2這個類出現(xiàn)先被unload贰盗,后來又會preload的情況。
當然舵盈,真的發(fā)生這種情況時(MyCls和MyCls2的生命周期不同)秽晚,我們必須檢查緩存過的弱引用是指向活動的類對象,還是指向一個已經被GC給unload的類對象菩浙。下一節(jié)將告訴你怎么樣檢查弱引用是否活動句伶。
5.1.4 引用比較
給定兩個引用(不管是全局考余、局部還是弱引用),你可以使用IsSameObject來判斷它們兩個是否指向相同的對象疫蔓。例如:
(env)->IsSameObject(env, obj1, obj2)
如果obj1和obj2指向相同的對象身冬,上面的調用返回JNI_TRUE(或者1)吏恭,否則返回JNI_FALSE(或者0)。
JNI中的一個引用NULL指向JVM中的null對象哀九。如果obj是一個局部或者全局引用阅束,你可以使用(env)->IsSameObject(env, obj, NULL)或者obj == NULL來判斷obj是否指向一個null對象。
在這一點兒上蝇更,弱引用有些有同,一個NULL弱引用同樣指向一個JVM中的null對象年扩,但不同的是,在一個弱引用上面使用IsSameObject時,返回值的意義是不同的:
(env)->IsSameObject(env, wobj, NULL)
上面的調用中厨幻,如果wobj已經被回收,會返回JNI_TRUE况脆,如果wobj仍然指向一個活動對象,會返回JNI_FALSE批糟。
5.2 釋放引用
每一個JNI引用被建立時,除了它所指向的JVM中的對象以外徽鼎,引用本身也會消耗掉一個數(shù)量的內存盛末。作為一個JNI程序員,你應該對程序在一個給定時間段內使用的引用數(shù)量十分小心满败。短時間內創(chuàng)建大量不會被立即回收的引用會導致內存溢出。
5.2.1 釋放局部引用
大部分情況下叹括,你在實現(xiàn)一個本地方法時不必擔心局部引用的釋放問題汁雷,因為本地方法被調用完成后侠讯,JVM會自動回收這些局部引用厢漩。盡管如此,以下幾種情況下架谎,為了避免內存溢出,JNI程序員應該手動釋放局部引用:
1会涎、 在實現(xiàn)一個本地方法調用時末秃,你需要創(chuàng)建大量的局部引用蛔溃。這種情況可能會導致JNI局部引用表的溢出,所以,最好是在局部引用不需要時立即手動刪除涧衙。比如弧哎,在下面的代碼中,本地代碼遍歷一個大的字符串數(shù)組序攘,每遍歷一個元素程奠,都會創(chuàng)建一個局部引用,當對這個元素的遍歷完成時距境,這個局部引用就不再需要了晶姊,你應該手動釋放它:
for (i = 0; i < len; i++) {
jstring jstr = (env)->GetObjectArrayElement(env, arr, i);
... / process jstr /
(env)->DeleteLocalRef(env, jstr);
}
2们衙、 你想寫一個工具函數(shù),這個函數(shù)被誰調用你是不知道的忆蚀。4.3節(jié)中的MyNewString演示了怎么樣在工具函數(shù)中使用引用后,使用DeleteLocalRef刪除欣鳖。不這樣做的話泽台,每次MyNewString被調用完成后,就會有兩個引用仍然占用空間蜕依。
3、 你的本地方法不會返回任何東西吹缔。例如茶没,一個本地方法可能會在一個事件接收循環(huán)里面被調用,這種情況下笛求,為了不讓局部引用累積造成內存溢出,手動釋放也是必須的蜂嗽。
4、 你的本地方法訪問一個大對象病附,因此創(chuàng)建了一個對這個大對象的引用。然后本地方法在返回前會有一個做大量的計算過程较剃,而在這個過程中是不需要前面創(chuàng)建的對大對象的引用的写穴。但是偿短,計算過程,對大對象的引用會阻止GC回收大對象婆排。
在下面的程序中,因為預先有一個明顯的DeleteLocalRef操作,在函數(shù)lengthyComputation的執(zhí)行過程中炕婶,GC可能會釋放由引用lref指向的對象锁施。
5.2.2 管理局部引用
JDK提供了一系列的函數(shù)來管理局部引用的生命周期肩狂。這些函數(shù)包括:EnsureLocalCapacity、NewLocalRef审磁、PushLocalFrame费什、PopLocalFrame瘩蚪。
JNI規(guī)范中指出,VM會確保每個本地方法可以創(chuàng)建至少16個局部引用琢锋。經驗表明,這個數(shù)量已經滿足大多數(shù)不需要和JVM中的內部對象有太多交互的本地方法。如果真的需要創(chuàng)建更多的引用,本地方法可以通過調用EnsureLocalCapacity來支持更多的局部引用。在下面的代碼中,對前面的例子做了些修改猩系,不考慮內存因素的情況下疗涉,它可以為創(chuàng)建大量的局部引用提供足夠的空間。
· /* 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 /
· }
當然武学,上面這個版本中沒有立即刪除不使用的局部引用硼补,因此會比前面的版本消耗更多的內存。
另外,Push/PopLocalFrame函數(shù)對允許程序員創(chuàng)建作用范圍層層嵌套的局部引用。例如,我們可以把上面的代碼重寫:
· #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);
· }
PushLocalFrame為一定數(shù)量的局部引用創(chuàng)建了一個使用堆棧氮墨,而PopLocalFrame負責銷毀堆棧頂端的引用。
Push/PopLocalFrame函數(shù)對提供了對局部引用的生命周期更方便的管理。上面的例子中,如果處理jstr的過程中創(chuàng)建了局部引用苛萎,則PopLocalFrame執(zhí)行時腌歉,這些局部引用全部會被銷毀馍驯。
當你寫一個會返回局部引用的工具函數(shù)時玛痊,NewLocalRef非常有用,我們會在5.3節(jié)中演示NewLocalRef的使用狂打。
本地代碼可能會創(chuàng)建大量的局部引用擂煞,其數(shù)量可能會超過16個或PushLocaFrame和EnsureLocalCapacity調用設置的個數(shù)。VM可能會嘗試分配足夠的內存菱父,但不能夠保證分配成功颈娜。如果失敗,VM會退出浙宜。
5.2.3 釋放全局引用
當你的本地代碼不再需要一個全局引用時官辽,你應該調用DeleteGlobalRef來釋放它。如果你沒有調用這個函數(shù)粟瞬,即使這個對象已經沒用了同仆,JVM也不會回收這個全局引用所指向的對象。
當你的本地代碼不再需要一個弱引用時裙品,應該調用DeleteWeakGlobalRef來釋放它俗批,如果你沒有調用這個函數(shù),JVM仍會回收弱引用所指向的對象市怎,但弱引用本身在引用表中所占的內存永遠也不會被回收岁忘。
5.3 管理引用的規(guī)則
前面已經做了一個全面的介紹,現(xiàn)在我們可以總結一下JNI引用的管理規(guī)則了区匠,目標就是減少內存使用和對象被引用保持而不能釋放干像。
通常情況下,有兩種本地代碼:直接實現(xiàn)本地方法的本地代碼和可以被使用在任何環(huán)境下的工具函數(shù)驰弄。
當編寫實現(xiàn)本地方法的本地代碼時麻汰,當心不要造成全局引用和弱引用的累加,因為本地方法執(zhí)行完畢后戚篙,這兩種引用不會被自動釋放五鲫。
當編寫一個工具函數(shù)的本地代碼時,當心不要在函數(shù)的調用軌跡上面遺漏任何的局部引用岔擂,因為工具函數(shù)被調用的場合是不確定的位喂,一旦被大量調用,很有可能造成內存溢出乱灵。
編寫工具函數(shù)時忆某,請遵守下面的規(guī)則:
1、 一個返回值為基本類型的工具函數(shù)被調用時阔蛉,它決不能造成局部、全局癞埠、弱引用不被回收的累加状原。
2聋呢、 當一個返回值為引用類型的工具函數(shù)被調用時,它除了返回的引用以外颠区,它決不能造成其它局部削锰、全局、弱引用的累加毕莱。
對工具函數(shù)來說器贩,為了使用緩存技術而創(chuàng)建一些全局引用或者弱引用是正常的。
如果一個工具函數(shù)返回一個引用朋截,你應該詳細說明返回的引用的類型蛹稍,以便于調用者更好地管理它們。下面的代碼中部服,頻繁地調用工具函數(shù)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. /
· }
函數(shù)NewLocalRef有時被用來確保一個工具函數(shù)返回一個局部引用廓八。為了演示這個用法奉芦,我們對MyNewString函數(shù)做了一些修改。下面的版本把一個被頻繁調用的字符串“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;
· }
在管理局部引用的生命周期中剧蹂,Push/PopLocalFrame是非常方便的声功。你可以在本地函數(shù)的入口處調用PushLocalFrame,然后在出口處調用PopLocalFrame宠叼,這樣的話先巴,在函數(shù)對中間任何位置創(chuàng)建的局部引用都會被釋放。而且车吹,這兩個函數(shù)是非常高效的筹裕,強烈建議使用它們。
如果你在函數(shù)的入口處調用了PushLocalFrame窄驹,記住在所有的出口(有return出現(xiàn)的地方)調用PopLocalFrame朝卒。在下面的代碼中,對PushLocalFrame的調用只有一次乐埠,但對PopLocalFrame的調用卻需要多次抗斤。
· 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;
· }
上面的代碼同樣演示了函數(shù)PopLocalFrame的第二個參數(shù)的用法。局部引用result一開始在PushLocalFrame創(chuàng)建的當前frame里面被創(chuàng)建丈咐,而把result傳入PopLocalFrame中時瑞眼,PopLocalFrame在彈出當前的frame前,會由result生成一個新的局部引用棵逊,再把這個新生成的局部引用存儲在上一個frame當中伤疙。