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

https://blog.csdn.net/xyang81/article/details/44657385


轉(zhuǎn)載請(qǐng)注明出處:http://blog.csdn.net/xyang81/article/details/44657385

這篇文章比較偏理論涝影,詳細(xì)介紹了在編寫本地代碼時(shí)三種引用的使用場(chǎng)景和注意事項(xiàng)蜡感∩氪可能看起來有點(diǎn)枯燥笛粘,但引用是在JNI中最容易出錯(cuò)的一個(gè)點(diǎn)趁怔,如果使用不當(dāng)湿硝,容易使程序造成內(nèi)存溢出,程序崩潰等現(xiàn)象润努。所以講得比較細(xì)关斜,有些地方看起來可能比較啰嗦,還請(qǐng)輕啪任连!《Android JNI局部引用表溢出:local reference table overflow (max=512)》這篇文章是一個(gè)JNI引用使用不當(dāng)造成引用表溢出蚤吹,最終導(dǎo)致程序崩潰的例子例诀。建議看完這篇文章之后随抠,再去看。

做Java的朋友都知道繁涂,在編碼的過程當(dāng)中拱她,內(nèi)存管理這一塊完全是透明的。new一個(gè)類的實(shí)例時(shí)扔罪,只知道創(chuàng)建完這個(gè)類的實(shí)例之后秉沼,會(huì)返回這個(gè)實(shí)例的一個(gè)引用,然后就可以拿著這個(gè)引用訪問它的所有數(shù)據(jù)成員了(屬性矿酵、方法)唬复。完全不用管JVM內(nèi)部是怎么實(shí)現(xiàn)的,如何為新創(chuàng)建的對(duì)象來申請(qǐng)內(nèi)存全肮,也不用管對(duì)象使用完之后內(nèi)存是怎么釋放的敞咧,只需知道有一個(gè)垃圾回器在幫忙管理這些事情就OK的了。有經(jīng)驗(yàn)的朋友也許知道啟動(dòng)一個(gè)Java程序辜腺,如果沒有手動(dòng)創(chuàng)建其它線程休建,默認(rèn)會(huì)有兩個(gè)線程在跑,一個(gè)是main線程评疗,另一個(gè)就是GC線程(負(fù)責(zé)將一些不再使用的對(duì)象回收)测砂。如果你曾經(jīng)是做Java的然后轉(zhuǎn)去做C++,會(huì)感覺很“蛋疼”百匆,在C++中new一個(gè)對(duì)象砌些,使用完了還要做一次delete操作,malloc一次同樣也要調(diào)用free來釋放相應(yīng)的內(nèi)存加匈,否則你的程序就會(huì)有內(nèi)存泄露了存璃。而且在C/C++中內(nèi)存還分棧空間和堆空間矩动,其中局部變量有巧、函數(shù)形參變量、for中定義的臨時(shí)變量所分配的內(nèi)存空間都是存放在棻唬空間(而且還要注意大小的限制)篮迎,用new和malloc申請(qǐng)的內(nèi)存都存放在堆空間男图。。甜橱。但C/C++里的內(nèi)存管理還遠(yuǎn)遠(yuǎn)不止這些逊笆,這些只是最基礎(chǔ)的內(nèi)存管理常識(shí)。做Java的童鞋聽到這些肯定會(huì)偷樂了岂傲,咱寫Java的時(shí)候這些都不用管难裆,全都交給GC就萬事無優(yōu)了。手動(dòng)管理內(nèi)存雖然麻煩镊掖,而且需要特別細(xì)心乃戈,一不小心就有可能造成內(nèi)存泄露和野指針訪問等程序致命的問題,但凡事都有利弊亩进,手動(dòng)申請(qǐng)和釋放內(nèi)存對(duì)程序的掌握比較靈活症虑,不會(huì)受到平臺(tái)的限制。比如我們寫Android程序的時(shí)候归薛,內(nèi)存使用就受Dalivk虛擬機(jī)的限制谍憔,從最初版本的16~24M,到后來的32M到64M主籍,可能隨著以后移動(dòng)設(shè)備物理內(nèi)存的不大擴(kuò)大习贫,后面的Android版本內(nèi)存限制可能也會(huì)隨著提高。但在C/C++這層千元,就完全不受虛擬機(jī)的限制了苫昌。比如要在Android中要存儲(chǔ)一張超高清的圖片,剛好這張圖片的大小超過了Dalivk虛擬機(jī)對(duì)每個(gè)應(yīng)用的內(nèi)存大小限制诅炉,Java此時(shí)就顯得無能為力了蜡歹,但在C/C++看來就是小菜一碟了,malloc(1024*1024*50)涕烧,要多少內(nèi)存月而,您說個(gè)數(shù)。议纯。父款。C/C++程序員得意的說道~~Java不是說是一門純面象對(duì)象的語言嗎,所以除了基本數(shù)據(jù)類型外瞻凤,其它任何類型所創(chuàng)建的對(duì)象憨攒,JVM所申請(qǐng)的內(nèi)存都存在堆空間。上面提高到了GC阀参,是負(fù)責(zé)回收不再使用的對(duì)象肝集,它的全稱是Garbage Collection,也就是所謂的垃圾回收蛛壳。JVM會(huì)在適當(dāng)?shù)臅r(shí)機(jī)觸發(fā)GC操作杏瞻,一旦進(jìn)行GC操作所刀,就會(huì)將一些不再使用的對(duì)象進(jìn)行回收。那么哪些對(duì)象會(huì)被認(rèn)為是不再使用捞挥,并且可以被回收的呢浮创?我們來看下面二張圖:(注:圖摘自博主郭霖的《Android最佳性能實(shí)踐(二)——分析內(nèi)存的使用情況》

上圖當(dāng)中,每個(gè)藍(lán)色的圓圈就代表一個(gè)內(nèi)存當(dāng)中的對(duì)象砌函,而圓圈之間的箭頭就是它們的引用關(guān)系斩披。這些對(duì)象有些是處于活動(dòng)狀態(tài)的,而有些就已經(jīng)不再被使用了讹俊。那么GC操作會(huì)從一個(gè)叫作Roots的對(duì)象開始檢查垦沉,所有它可以訪問到的對(duì)象就說明還在使用當(dāng)中,應(yīng)該進(jìn)行保留劣像,而其它的對(duì)象就表示已經(jīng)不再被使用了乡话,如下圖所示:

可以看到摧玫,目前所有黃色的對(duì)象都處于活動(dòng)狀態(tài)耳奕,仍然會(huì)被系統(tǒng)繼續(xù)保留,而藍(lán)色的對(duì)象就會(huì)在GC操作當(dāng)中被系統(tǒng)回收掉了诬像,這就是JVM執(zhí)行一次GC的簡(jiǎn)單流程屋群。

????上面說的廢話好像有點(diǎn)多哈,下面進(jìn)入正題坏挠。通過上面的討論芍躏,大家都知道,如果一個(gè)Java對(duì)象沒有被其它成員變量或靜態(tài)變量所引用的話降狠,就隨時(shí)有可能會(huì)被GC回收掉对竣。所以我們?cè)诰帉懕镜卮a時(shí),要注意從JVM中獲取到的引用在使用時(shí)被GC回收的可能性榜配。由于本地代碼不能直接通過引用操作JVM內(nèi)部的數(shù)據(jù)結(jié)構(gòu)否纬,要進(jìn)行這些操作必須調(diào)用相應(yīng)的JNI接口來間接操作所引用的數(shù)據(jù)結(jié)構(gòu)。JNI提供了和Java相對(duì)應(yīng)的引用類型蛋褥,供本地代碼配合JNI接口間接操作JVM內(nèi)部的數(shù)據(jù)內(nèi)容使用临燃。如:jobject、jstring烙心、jclass膜廊、jarray、jintArray等淫茵。因?yàn)槲覀冎煌ㄟ^JNI接口操作JNI提供的引用類型數(shù)據(jù)結(jié)構(gòu)爪瓜,而且每個(gè)JVM都實(shí)現(xiàn)了JNI規(guī)范相應(yīng)的接口,所以我們不必?fù)?dān)心特定JVM中對(duì)象的存儲(chǔ)方式和內(nèi)部數(shù)據(jù)結(jié)構(gòu)等信息匙瘪,我們只需要學(xué)習(xí)JNI中三種不同的引用即可铆铆。

由于Java程序運(yùn)行在虛擬機(jī)中的這個(gè)特點(diǎn)炬转,在Java中創(chuàng)建的對(duì)象、定義的變量和方法算灸,內(nèi)部對(duì)象的數(shù)據(jù)結(jié)構(gòu)是怎么定義的扼劈,只有JVM自己知道。如果我們?cè)贑/C++中想要訪問Java中對(duì)象的屬性和方法時(shí)菲驴,是不能夠直接操作JVM內(nèi)部Java對(duì)象的數(shù)據(jù)結(jié)構(gòu)的荐吵。想要在C/C++中正確的訪問Java的數(shù)據(jù)結(jié)構(gòu),JVM就必須有一套規(guī)則來約束C/C++與Java互相訪問的機(jī)制赊瞬,所以才有了JNI規(guī)范先煎,JNI規(guī)范定義了一系列接口,任何實(shí)現(xiàn)了這套JNI接口的Java虛擬機(jī)巧涧,C/C++就可以通過調(diào)用這一系列接口來間接的訪問Java中的數(shù)據(jù)結(jié)構(gòu)薯蝎。比如前面文章中學(xué)習(xí)到的常用JNI接口有:GetStringUTFChars(從Java虛擬機(jī)中獲取一個(gè)字符串)、ReleaseStringUTFChars(釋放從JVM中獲取字符串所分配的內(nèi)存空間)谤绳、NewStringUTF占锯、GetArrayLength、GetFieldID缩筛、GetMethodID消略、FindClass等。

三種引用簡(jiǎn)介及區(qū)別

在JNI規(guī)范中定義了三種引用:局部引用(Local Reference)瞎抛、全局引用(Global Reference)艺演、弱全局引用(Weak Global Reference)。區(qū)別如下:

1桐臊、局部引用:通過NewLocalRef和各種JNI接口創(chuàng)建(FindClass胎撤、NewObject、GetObjectClass和NewCharArray等)断凶。會(huì)阻止GC回收所引用的對(duì)象伤提,不在本地函數(shù)中跨函數(shù)使用,不能跨線前使用懒浮。函數(shù)返回后局部引用所引用的對(duì)象會(huì)被JVM自動(dòng)釋放飘弧,或調(diào)用DeleteLocalRef釋放。(*env)->DeleteLocalRef(env,local_ref)

jclasscls_string=(*env)->FindClass(env,"java/lang/String");jcharArraycharArr=(*env)->NewCharArray(env, len);jstringstr_obj=(*env)->NewObject(env, cls_string, cid_string, elemArray);jstringstr_obj_local_ref=(*env)->NewLocalRef(env,str_obj);//通過NewLocalRef函數(shù)創(chuàng)建...

1

2

3

4

5

2砚著、全局引用:調(diào)用NewGlobalRef基于局部引用創(chuàng)建次伶,會(huì)阻GC回收所引用的對(duì)象』拢可以跨方法冠王、跨線程使用。JVM不會(huì)自動(dòng)釋放舌镶,必須調(diào)用DeleteGlobalRef手動(dòng)釋放(*env)->DeleteGlobalRef(env,g_cls_string);

static jclassg_cls_string;void TestFunc(JNIEnv* env, jobjectobj) {jclasscls_string=(*env)->FindClass(env,"java/lang/String");g_cls_string = (*env)->NewGlobalRef(env,cls_string);}

1

2

3

4

5

3柱彻、?弱全局引用:調(diào)用NewWeakGlobalRef基于局部引用或全局引用創(chuàng)建豪娜,不會(huì)阻止GC回收所引用的對(duì)象,可以跨方法哟楷、跨線程使用瘤载。引用不會(huì)自動(dòng)釋放,在JVM認(rèn)為應(yīng)該回收它的時(shí)候(比如內(nèi)存緊張的時(shí)候)進(jìn)行回收而被釋放卖擅∶迹或調(diào)用DeleteWeakGlobalRef手動(dòng)釋放。(*env)->DeleteWeakGlobalRef(env,g_cls_string)

static jclassg_cls_string;void TestFunc(JNIEnv* env, jobjectobj) {jclasscls_string=(*env)->FindClass(env,"java/lang/String");g_cls_string = (*env)->NewWeakGlobalRef(env,cls_string);}

1

2

3

4

5

局部引用

局部引用也稱本地引用惩阶,通常是在函數(shù)中創(chuàng)建并使用挎狸。會(huì)阻止GC回收所引用的對(duì)象。比如断楷,調(diào)用NewObject接口創(chuàng)建一個(gè)新的對(duì)象實(shí)例并返回一個(gè)對(duì)這個(gè)對(duì)象的局部引用锨匆。局部引用只有在創(chuàng)建它的本地方法返回前有效,本地方法返回到Java層之后冬筒,如果Java層沒有對(duì)返回的局部引用使用的話恐锣,局部引用就會(huì)被JVM自動(dòng)釋放。你可能會(huì)為了提高程序的性能账千,在函數(shù)中將局部引用存儲(chǔ)在靜態(tài)變量中緩存起來侥蒙,供下次調(diào)用時(shí)使用。這種方式是錯(cuò)誤的匀奏,因?yàn)楹瘮?shù)返回后局部引很可能馬上就會(huì)被釋放掉,靜態(tài)變量中存儲(chǔ)的就是一個(gè)被釋放后的內(nèi)存地址学搜,成了一個(gè)野針對(duì)娃善,下次再使用的時(shí)候就會(huì)造成非法地址的訪問,使程序崩潰瑞佩。請(qǐng)看下面一個(gè)例子聚磺,錯(cuò)誤的緩存了String的Class引用:

/*錯(cuò)誤的局部引用*/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;// 注意:錯(cuò)誤的引用緩存if(cls_string==NULL) {? ? ? ? cls_string=(*env)->FindClass(env,"java/lang/String");if(cls_string==NULL) {returnNULL;? ? ? ? }? ? }// 緩存String的構(gòu)造方法IDif(cid_string==NULL) {? ? ? ? cid_string=(*env)->GetMethodID(env, cls_string,"<init>","([C)V");if(cid_string==NULL) {returnNULL;? ? ? ? }? ? }//省略額外的代碼.......elemArray=(*env)->NewCharArray(env, len);// ....j_str=(*env)->NewObject(env, cls_string, cid_string, elemArray);// 釋放局部引用(*env)->DeleteLocalRef(env, elemArray);returnj_str;}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

上面代碼中,我們省略了和我們討論無關(guān)的代碼炬丸。因?yàn)镕indClass返回一個(gè)對(duì)java.lang.String對(duì)象的局部引用瘫寝,上面代碼中緩存cls_string做法是錯(cuò)誤的。假設(shè)一個(gè)本地方法C.f調(diào)用了newString:

JNIEXPORT jstring JNICALL Java_C_f(JNIEnv *env, jobject this) {? ? char *c_str =...;...returnnewString(c_str);}

1

2

3

4

5

6

7

Java_com_study_jnilearn_AccessCache_newString 下面簡(jiǎn)稱newString

C.f方法返回后稠炬,JVM會(huì)釋放在這個(gè)方法執(zhí)行期間創(chuàng)建的所有局部引用焕阿,也包含對(duì)String的Class引用cls_string。當(dāng)再次調(diào)用newString時(shí)首启,newString所指向引用的內(nèi)存空間已經(jīng)被釋放暮屡,成為了一個(gè)野指針,再訪問這個(gè)指針的引用時(shí)毅桃,會(huì)導(dǎo)致因非法的內(nèi)存訪問造成程序崩潰褒纲。

......= C.f(); // 第一次調(diào)是OK的...= C.f(); // 第二次調(diào)用時(shí)准夷,訪問的是一個(gè)無效的引用....

1

2

3

4

釋放局部引用

釋放一個(gè)局部引用有兩種方式,一個(gè)是本地方法執(zhí)行完畢后JVM自動(dòng)釋放莺掠,另外一個(gè)是自己調(diào)用DeleteLocalRef手動(dòng)釋放衫嵌。既然JVM會(huì)在函數(shù)返回后會(huì)自動(dòng)釋放所有局部引用,為什么還需要手動(dòng)釋放呢彻秆?大部分情況下渐扮,我們?cè)趯?shí)現(xiàn)一個(gè)本地方法時(shí)不必?fù)?dān)心局部引用的釋放問題,函數(shù)被調(diào)用完成后掖棉,JVM 會(huì)自動(dòng)釋放函數(shù)中創(chuàng)建的所有局部引用墓律。盡管如此,以下幾種情況下幔亥,為了避免內(nèi)存溢出耻讽,我們應(yīng)該手動(dòng)釋放局部引用:

1、JNI會(huì)將創(chuàng)建的局部引用都存儲(chǔ)在一個(gè)局部引用表中帕棉,如果這個(gè)表超過了最大容量限制针肥,就會(huì)造成局部引用表溢出,使程序崩潰香伴。經(jīng)測(cè)試慰枕,Android上的JNI局部引用表最大數(shù)量是512個(gè)。當(dāng)我們?cè)趯?shí)現(xiàn)一個(gè)本地方法時(shí)即纲,可能需要?jiǎng)?chuàng)建大量的局部引用具帮,如果沒有及時(shí)釋放,就有可能導(dǎo)致JNI局部引用表的溢出低斋,所以蜂厅,在不需要局部引用時(shí)就立即調(diào)用DeleteLocalRef手動(dòng)刪除。比如,在下面的代碼中,本地代碼遍歷一個(gè)特別大的字符串?dāng)?shù)組扶认,每遍歷一個(gè)元素,都會(huì)創(chuàng)建一個(gè)局部引用稠通,當(dāng)對(duì)使用完這個(gè)元素的局部引用時(shí),就應(yīng)該馬上手動(dòng)釋放它买猖。

for(i =0; i < len; i++) {? ? jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);.../* 使用jstr */? ? (*env)->DeleteLocalRef(env, jstr); // 使用完成之后馬上釋放}

1

2

3

4

5

2改橘、在編寫JNI工具函數(shù)時(shí),工具函數(shù)在程序當(dāng)中是公用的政勃,被誰調(diào)用你是不知道的唧龄。上面newString這個(gè)函數(shù)演示了怎么樣在工具函數(shù)中使用完局部引用后,調(diào)用DeleteLocalRef刪除。不這樣做的話既棺,每次調(diào)用newString之后讽挟,都會(huì)遺留兩個(gè)引用占用空間(elemArray和cls_string,cls_string不用static緩存的情況下)丸冕。

3耽梅、如果你的本地函數(shù)不會(huì)返回。比如一個(gè)接收消息的函數(shù)胖烛,里面有一個(gè)死循環(huán)眼姐,用于等待別人發(fā)送消息過來while(true) { if (有新的消息) { 處理之。佩番。众旗。。} else { 等待新的消息趟畏。贡歧。。}}赋秀。如果在消息循環(huán)當(dāng)中創(chuàng)建的引用你不顯示刪除利朵,很快將會(huì)造成JVM局部引用表溢出。

4猎莲、局部引用會(huì)阻止所引用的對(duì)象被GC回收绍弟。比如你寫的一個(gè)本地函數(shù)中剛開始需要訪問一個(gè)大對(duì)象,因此一開始就創(chuàng)建了一個(gè)對(duì)這個(gè)對(duì)象的引用著洼,但在函數(shù)返回前會(huì)有一個(gè)大量的非常復(fù)雜的計(jì)算過程樟遣,而在這個(gè)計(jì)算過程當(dāng)中是不需要前面創(chuàng)建的那個(gè)大對(duì)象的引用的。但是郭脂,在計(jì)算的過程當(dāng)中年碘,如果這個(gè)大對(duì)象的引用還沒有被釋放的話,會(huì)阻止GC回收這個(gè)對(duì)象展鸡,內(nèi)存一直占用者,造成資源的浪費(fèi)埃难。所以這種情況下莹弊,在進(jìn)行復(fù)雜計(jì)算之前就應(yīng)該把引用給釋放了,以免不必要的資源浪費(fèi)涡尘。

/* 假如這是一個(gè)本地方法實(shí)現(xiàn) */JNIEXPORT void JNICALL Java_pkg_Cls_func(JNIEnv *env, jobject this){? lref =.../* lref引用的是一個(gè)大的Java對(duì)象 */.../* 在這里已經(jīng)處理完業(yè)務(wù)邏輯后忍弛,這個(gè)對(duì)象已經(jīng)使用完了 */? (*env)->DeleteLocalRef(env, lref); /* 及時(shí)刪除這個(gè)對(duì)這個(gè)大對(duì)象的引用,GC就可以對(duì)它回收考抄,并釋放相應(yīng)的資源*/? lengthyComputation();? /* 在里有個(gè)比較耗時(shí)的計(jì)算過程 */return;? ? ? ? ? ? ? ? /* 計(jì)算完成之后细疚,函數(shù)返回之前所有引用都已經(jīng)釋放 */}

1

2

3

4

5

6

7

8

9

管理局部引用

JNI提供了一系列函數(shù)來管理局部引用的生命周期。這些函數(shù)包括:EnsureLocalCapacity川梅、NewLocalRef疯兼、PushLocalFrame然遏、PopLocalFrame、DeleteLocalRef吧彪。JNI規(guī)范指出待侵,任何實(shí)現(xiàn)JNI規(guī)范的JVM,必須確保每個(gè)本地函數(shù)至少可以創(chuàng)建16個(gè)局部引用(可以理解為虛擬機(jī)默認(rèn)支持創(chuàng)建16個(gè)局部引用)姨裸。實(shí)際經(jīng)驗(yàn)表明秧倾,這個(gè)數(shù)量已經(jīng)滿足大多數(shù)不需要和JVM中內(nèi)部對(duì)象有太多交互的本地方函數(shù)。如果需要?jiǎng)?chuàng)建更多的引用傀缩,可以通過調(diào)用EnsureLocalCapacity函數(shù)那先,確保在當(dāng)前線程中創(chuàng)建指定數(shù)量的局部引用,如果創(chuàng)建成功則返回0赡艰,否則創(chuàng)建失敗售淡,并拋出OutOfMemoryError異常。EnsureLocalCapacity這個(gè)函數(shù)是1.2以上版本才提供的瞄摊,為了向下兼容勋又,在編譯的時(shí)候,如果申請(qǐng)創(chuàng)建的局部引用超過了本地引用的最大容量换帜,在運(yùn)行時(shí)JVM會(huì)調(diào)用FatalError函數(shù)使程序強(qiáng)制退出楔壤。在開發(fā)過程當(dāng)中,可以為JVM添加-verbose:jni參數(shù)惯驼,在編譯的時(shí)如果發(fā)現(xiàn)本地代碼在試圖申請(qǐng)過多的引用時(shí)蹲嚣,會(huì)打印警告信息提示我們要注意。在下面的代碼中祟牲,遍歷數(shù)組時(shí)會(huì)獲取每個(gè)元素的引用隙畜,使用完了之后不手動(dòng)刪除,不考慮內(nèi)存因素的情況下说贝,它可以為這種創(chuàng)建大量的局部引用提供足夠的空間议惰。由于沒有及時(shí)刪除局部引用,因此在函數(shù)執(zhí)行期間乡恕,會(huì)消耗更多的內(nèi)存言询。

/*處理函數(shù)邏輯時(shí),確保函數(shù)能創(chuàng)建len個(gè)局部引用*/if((*env)->EnsureLocalCapacity(env,len) !=0) {.../*申請(qǐng)len個(gè)局部引用的內(nèi)存空間失敗 OutOfMemoryError*/return;}for(i=0; i < len; i++) {? ? jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);? ? //...使用jstr字符串? ? /*這里沒有刪除在for中臨時(shí)創(chuàng)建的局部引用*/}

1

2

3

4

5

6

7

8

9

10

另外傲宜,除了EnsureLocalCapacity函數(shù)可以擴(kuò)充指定容量的局部引用數(shù)量外运杭,我們也可以利用Push/PopLocalFrame函數(shù)對(duì)創(chuàng)建作用范圍層層嵌套的局部引用。例如函卒,我們把上面那段處理字符串?dāng)?shù)組的代碼用Push/PopLocalFrame函數(shù)對(duì)重寫:

#define N_REFS ... /*最大局部引用數(shù)量*/for(i =0; i < len; i++) {if((*env)->PushLocalFrame(env, N_REFS) !=0) {.../*內(nèi)存溢出*/? ? }? ? jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);.../* 使用jstr */? ? (*env)->PopLocalFrame(env,NULL);}

1

2

3

4

5

6

7

8

9

PushLocalFrame為當(dāng)前函數(shù)中需要用到的局部引用創(chuàng)建了一個(gè)引用堆棧辆憔,(如果之前調(diào)用PushLocalFrame已經(jīng)創(chuàng)建了Frame,在當(dāng)前的本地引用棧中仍然是有效的)每遍歷一次調(diào)用(*env)->GetObjectArrayElement(env, arr, i);返回一個(gè)局部引用時(shí),JVM會(huì)自動(dòng)將該引用壓入當(dāng)前局部引用棧中虱咧。而PopLocalFrame負(fù)責(zé)銷毀棧中所有的引用熊榛。這樣一來,Push/PopLocalFrame函數(shù)對(duì)提供了對(duì)局部引用生命周期更方便的管理彤钟,而不需要時(shí)刻關(guān)注獲取一個(gè)引用后来候,再調(diào)用DeleteLocalRef來釋放引用。在上面的例子中逸雹,如果在處理jstr的過程當(dāng)中創(chuàng)建了局部引用营搅,則PopLocalFrame執(zhí)行時(shí),這些局部引用將全都會(huì)被銷毀梆砸。在調(diào)用PopLocalFrame銷毀當(dāng)前frame中的所有引用前转质,如果第二個(gè)參數(shù)result不為空,會(huì)由result生成一個(gè)新的局部引用帖世,再把這個(gè)新生成的局部引用存儲(chǔ)在上一個(gè)frame中休蟹。請(qǐng)看下面的示例:

// 函數(shù)原型jobject (JNICALL *PopLocalFrame)(JNIEnv *env, jobject result);jstring other_jstr;for(i =0; i < len; i++) {if((*env)->PushLocalFrame(env, N_REFS) !=0) {.../*內(nèi)存溢出*/? ? }? ? jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);.../* 使用jstr */if(i ==2) {? ? ? ? other_jstr = jstr;? ? }? ? other_jstr = (*env)->PopLocalFrame(env, other_jstr);? // 銷毀局部引用棧前返回指定的引用}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

還要注意的一個(gè)問題是,局部引用不能跨線程使用日矫,只在創(chuàng)建它的線程有效赂弓。不要試圖在一個(gè)線程中創(chuàng)建局部引用并存儲(chǔ)到全局引用中,然后在另外一個(gè)線程中使用哪轿。

全局引用

全局引用可以跨方法盈魁、跨線程使用,直到它被手動(dòng)釋放才會(huì)失效窃诉。同局部引用一樣杨耙,也會(huì)阻止它所引用的對(duì)象被GC回收。與局部引用創(chuàng)建方式不同的是飘痛,只能通過NewGlobalRef函數(shù)創(chuàng)建珊膜。下面這個(gè)版本的newString演示怎么樣使用一個(gè)全局引用:

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) {returnNULL;? ? ? ? }? ? ? ? // 將java.lang.String類的Class引用緩存到全局引用當(dāng)中? ? ? ? cls_string = (*env)->NewGlobalRef(env, local_cls_string);? ? ? ? // 刪除局部引用? ? ? ? (*env)->DeleteLocalRef(env, local_cls_string);? ? ? ? // 再次驗(yàn)證全局引用是否創(chuàng)建成功if(cls_string ==NULL) {returnNULL;? ? ? ? }? ? }? ? // ....returnjstr;}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

弱全局引用

弱全局引用使用NewGlobalWeakRef創(chuàng)建,使用DeleteGlobalWeakRef釋放宣脉。下面簡(jiǎn)稱弱引用车柠。與全局引用類似,弱引用可以跨方法塑猖、線程使用堪遂。但與全局引用很重要不同的一點(diǎn)是,弱引用不會(huì)阻止GC回收它引用的對(duì)象萌庆。在newString這個(gè)函數(shù)中,我們也可以使用弱引用來存儲(chǔ)String的Class引用币旧,因?yàn)閖ava.lang.String這個(gè)類是系統(tǒng)類践险,永遠(yuǎn)不會(huì)被GC回收。當(dāng)本地代碼中緩存的引用不一定要阻止GC回收它所指向的對(duì)象時(shí),弱引用就是一個(gè)最好的選擇巍虫。假設(shè)彭则,一個(gè)本地方法mypkg.MyCls.f需要緩存一個(gè)指向類mypkg.MyCls2的引用,如果在弱引用中緩存的話占遥,仍然允許mypkg.MyCls2這個(gè)類被unload俯抖,因?yàn)槿跻貌粫?huì)阻止GC回收所引用的對(duì)象。請(qǐng)看下面的代碼段:

JNIEXPORT void JNICALLJava_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這個(gè)類 */? ? ? ? }? ? ? ? myCls2 = NewWeakGlobalRef(env, myCls2Local);if(myCls2 ==NULL)? ? ? ? {return; /* 內(nèi)存溢出 */? ? ? ? }? ? }.../* 使用myCls2的引用 */}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

我們假設(shè)MyCls和MyCls2有相同的生命周期(例如瓦胎,他們可能被相同的類加載器加載)芬萍,因?yàn)槿跻玫拇嬖冢覀儾槐負(fù)?dān)心MyCls和它所在的本地代碼在被使用時(shí)搔啊,MyCls2這個(gè)類出現(xiàn)先被unload柬祠,后來又會(huì)preload的情況。當(dāng)然负芋,如果真的發(fā)生這種情況時(shí)(MyCls和MyCls2此時(shí)的生命周期不同)漫蛔,我們?cè)谑褂萌跻脮r(shí),必須先檢查緩存過的弱引用是指向活動(dòng)的類對(duì)象旧蛾,還是指向一個(gè)已經(jīng)被GC給unload的類對(duì)象莽龟。下面馬上告訴你怎樣檢查弱引用是否活動(dòng),即引用的比較锨天。

引用比較

給定兩個(gè)引用(不管是全局毯盈、局部還是弱全局引用),我們只需要調(diào)用IsSameObject來判斷它們兩個(gè)是否指向相同的對(duì)象绍绘。例如:(*env)->IsSameObject(env, obj1, obj2)奶镶,如果obj1和obj2指向相同的對(duì)象,則返回JNI_TRUE(或者1)陪拘,否則返回JNI_FALSE(或者0)厂镇。有一個(gè)特殊的引用需要注意:NULL,JNI中的NULL引用指向JVM中的null對(duì)象左刽。如果obj是一個(gè)局部或全局引用捺信,使用(*env)->IsSameObject(env, obj, NULL)?或者 obj == NULL 來判斷obj是否指向一個(gè)null對(duì)象即可。但需要注意的是欠痴,IsSameObject用于弱全局引用與NULL比較時(shí)迄靠,返回值的意義是不同于局部引用和全局引用的:

jobject local_obj_ref = (*env)->NewObject(env, xxx_cls,xxx_mid);jobject g_obj_ref = (*env)->NewWeakGlobalRef(env, local_ref);//...業(yè)務(wù)邏輯處理jboolean isEqual = (*env)->IsSameObject(env, g_obj_ref,NULL);

1

2

3

4

在上面的IsSameObject調(diào)用中,如果g_obj_ref指向的引用已經(jīng)被回收喇辽,會(huì)返回JNI_TRUE掌挚,如果wobj仍然指向一個(gè)活動(dòng)對(duì)象,會(huì)返回JNI_FALSE菩咨。

釋放全局引用

每一個(gè)JNI引用被建立時(shí)吠式,除了它所指向的JVM中對(duì)象的引用需要占用一定的內(nèi)存空間外陡厘,引用本身也會(huì)消耗掉一個(gè)數(shù)量的內(nèi)存空間。作為一個(gè)優(yōu)秀的程序員特占,我們應(yīng)該對(duì)程序在一個(gè)給定的時(shí)間段內(nèi)使用的引用數(shù)量要十分小心糙置。短時(shí)間內(nèi)創(chuàng)建大量而沒有被立即回收的引用很可能就會(huì)導(dǎo)致內(nèi)存溢出。

當(dāng)我們的本地代碼不再需要一個(gè)全局引用時(shí)是目,應(yīng)該馬上調(diào)用DeleteGlobalRef來釋放它谤饭。如果不手動(dòng)調(diào)用這個(gè)函數(shù),即使這個(gè)對(duì)象已經(jīng)沒用了懊纳,JVM也不會(huì)回收這個(gè)全局引用所指向的對(duì)象揉抵。

同樣,當(dāng)我們的本地代碼不再需要一個(gè)弱全局引用時(shí)长踊,也應(yīng)該調(diào)用DeleteWeakGlobalRef來釋放它功舀,如果不手動(dòng)調(diào)用這個(gè)函數(shù)來釋放所指向的對(duì)象,JVM仍會(huì)回收弱引用所指向的對(duì)象身弊,但弱引用本身在引用表中所占的內(nèi)存永遠(yuǎn)也不會(huì)被回收辟汰。

管理引用的規(guī)則

前面對(duì)三種引用已做了一個(gè)全面的介紹,下面來總結(jié)一下引用的管理規(guī)則和使用時(shí)的一些注意事項(xiàng)阱佛,使用好引用的目的就是為了減少內(nèi)存使用和對(duì)象被引用保持而不能釋放帖汞,造成內(nèi)存浪費(fèi)。所以在開發(fā)當(dāng)中要特別小心凑术!

通常情況下翩蘸,有兩種本地代碼使用引用時(shí)要注意:

1、 直接實(shí)現(xiàn)Java層聲明的native函數(shù)的本地代碼

當(dāng)編寫這類本地代碼時(shí)淮逊,要當(dāng)心不要造成全局引用和弱引用的累加催首,因?yàn)楸镜胤椒▓?zhí)行完畢后,這兩種引用不會(huì)被自動(dòng)釋放泄鹏。

2郎任、被用在任何環(huán)境下的工具函數(shù)。例如:方法調(diào)用备籽、屬性訪問和異常處理的工具函數(shù)等舶治。

編寫工具函數(shù)的本地代碼時(shí),要當(dāng)心不要在函數(shù)的調(diào)用軌跡上遺漏任何的局部引用车猬,因?yàn)楣ぞ吆瘮?shù)被調(diào)用的場(chǎng)合和次數(shù)是不確定的霉猛,一量被大量調(diào)用,就很有可能造成內(nèi)存溢出珠闰。所以在編寫工具函數(shù)時(shí)惜浅,請(qǐng)遵守下面的規(guī)則:

1> 一個(gè)返回值為基本類型的工具函數(shù)被調(diào)用時(shí),它決不能造成局部伏嗜、全局赡矢、弱全局引用被回收的累加

2> 當(dāng)一個(gè)返回值為引用類型的工具函數(shù)被調(diào)用時(shí)杭朱,它除了返回的引用以外,它決不能造成其它局部吹散、全局、弱引用的累加

對(duì)于工具函數(shù)來說八酒,為了使用緩存技術(shù)而創(chuàng)建一些全局引用或者弱全局引用是正常的空民。如果一個(gè)工具函數(shù)返回的是一個(gè)引用,我們應(yīng)該寫好注釋詳細(xì)說明返回引用的類型羞迷,以便于使用者更好的管理它們界轩。下面的代碼中,頻繁地調(diào)用工具函數(shù)GetInfoString衔瓮,我們需要知道GetInfoString返回引用的類型是什么浊猾,以便于每次使用完成后調(diào)用相應(yīng)的JNI函數(shù)來釋放掉它。

while(JNI_TRUE) {? ? jstring infoString = GetInfoString(info);.../* 處理infoString */? ? ??? /* 使用完成之后热鞍,調(diào)用DeleteLocalRef葫慎、DeleteGlobalRef、DeleteWeakGlobalRef哪一個(gè)函數(shù)來釋放這個(gè)引用呢薇宠?*/}

1

2

3

4

5

函數(shù)NewLocalRef有時(shí)被用來確保一個(gè)工具函數(shù)返回一個(gè)局部引用偷办。我們改造一下newString這個(gè)函數(shù),演示一下這個(gè)函數(shù)的用法澄港。下面的newString是把一個(gè)被頻繁調(diào)用的字符串“CommonString”緩存在了全局引用里:

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString{? ? static jstring result;? ? /* 使用wstrncmp函數(shù)比較兩個(gè)Unicode字符串 */if(wstrncmp("CommonString", chars, len) ==0)? ? {? ? ? ? /* 將"CommonString"這個(gè)字符串緩存到全局引用中 */? ? ? ? static jstring cachedString =NULL;if(cachedString ==NULL)? ? ? ? {? ? ? ? ? ? /* 先創(chuàng)建"CommonString"這個(gè)字符串 */? ? ? ? ? ? jstring cachedStringLocal =...;? ? ? ? ? ? /* 然后將這個(gè)字符串緩存到全局引用中 */? ? ? ? ? ? cachedString = (*env)->NewGlobalRef(env, cachedStringLocal);? ? ? ? }? ? ? ? // 基于全局引用創(chuàng)建一個(gè)局引用返回椒涯,也同樣會(huì)阻止GC回收所引用的這個(gè)對(duì)象,因?yàn)樗鼈冎赶虻氖峭粋€(gè)對(duì)象return(*env)->NewLocalRef(env, cachedString);? ? ? }...returnresult;}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

在管理局部引用的生命周期中回梧,Push/PopLocalFrame是非常方便且安全的废岂。我們可以在本地函數(shù)的入口處調(diào)用PushLocalFrame,然后在出口處調(diào)用PopLocalFrame狱意,這樣的話湖苞,在函數(shù)內(nèi)任何位置創(chuàng)建的局部引用都會(huì)被釋放。而且髓涯,這兩個(gè)函數(shù)是非常高效的袒啼,強(qiáng)烈建議使用它們。需要注意的是纬纪,如果在函數(shù)的入口處調(diào)用了PushLocalFrame蚓再,記住要在函數(shù)所有出口(有return語句出現(xiàn)的地方)都要調(diào)用PopLocalFrame。在下面的代碼中包各,對(duì)PushLocalFrame的調(diào)用只有一次摘仅,但調(diào)用PopLocalFrame確有多次,當(dāng)然你也可以使用goto語句來統(tǒng)一處理问畅。

jobject f(JNIEnv *env,...){? ? jobject result;if((*env)->PushLocalFrame(env,10) <0)? ? {? ? ? ? /* 調(diào)用PushLocalFrame獲取10個(gè)局部引用失敗娃属,不需要調(diào)用PopLocalFrame */returnNULL;? ? }...result =...; // 創(chuàng)建局部引用resultif(...)? ? {? ? ? ? /* 返回前先彈出棧頂?shù)膄rame */? ? ? ? result = (*env)->PopLocalFrame(env, result);returnresult;? ? }...result = (*env)->PopLocalFrame(env, result);? ? /* 正常返回 */returnresult;}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

上面的代碼同樣演示了函數(shù)PopLocalFrame的第二個(gè)參數(shù)的用法六荒,局部引用result一開始在PushLocalFrame創(chuàng)建在當(dāng)前frame里面,而把result傳入PopLocalFrame中時(shí)矾端,PopLocalFrame在彈出當(dāng)前的frame前掏击,會(huì)由result生成一個(gè)新的局部引用,再將這個(gè)新生成的局部引用存儲(chǔ)在上一個(gè)frame當(dāng)中秩铆。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末砚亭,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子殴玛,更是在濱河造成了極大的恐慌捅膘,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件滚粟,死亡現(xiàn)場(chǎng)離奇詭異寻仗,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)凡壤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門署尤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鲤遥,你說我怎么就攤上這事沐寺。” “怎么了盖奈?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵混坞,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我钢坦,道長(zhǎng)究孕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任爹凹,我火速辦了婚禮厨诸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘禾酱。我一直安慰自己微酬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布颤陶。 她就那樣靜靜地躺著颗管,像睡著了一般。 火紅的嫁衣襯著肌膚如雪滓走。 梳的紋絲不亂的頭發(fā)上垦江,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音搅方,去河邊找鬼比吭。 笑死绽族,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的衩藤。 我是一名探鬼主播吧慢,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼慷彤!你這毒婦竟也來了娄蔼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤底哗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后锚沸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體跋选,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年哗蜈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了前标。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡距潘,死狀恐怖炼列,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情音比,我是刑警寧澤俭尖,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站洞翩,受9級(jí)特大地震影響稽犁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜骚亿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一已亥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧来屠,春花似錦虑椎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至嫂粟,卻和暖如春娇未,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背星虹。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工零抬, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留镊讼,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓平夜,卻偏偏與公主長(zhǎng)得像蝶棋,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子忽妒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355