jni2
JNI 調(diào)用構(gòu)造方法和父類實(shí)例方法
在前面我們學(xué)習(xí)到了在 Native 層如何調(diào)用 Java 靜態(tài)方法和實(shí)例方法,其中調(diào)用實(shí)例方法的示例代碼中也提到了調(diào)用構(gòu)造函數(shù)來(lái)實(shí)始化一個(gè)對(duì)象,但沒(méi)有詳細(xì)介紹,一帶而過(guò)了。還沒(méi)有閱讀過(guò)的同學(xué)請(qǐng)移步《JNI——C/C++ 訪問(wèn) Java 實(shí)例方法和靜態(tài)方法》閱讀。這章詳細(xì)來(lái)介紹下初始一個(gè)對(duì)象的兩種方式,以及如何調(diào)用子類對(duì)象重寫的父類實(shí)例方法。
構(gòu)造方法和父類實(shí)例方法
我們先回過(guò)一下,在 Java 中實(shí)例化一個(gè)對(duì)象和調(diào)用父類實(shí)例方法的流程。先看一段代碼:
package com.study.jnilearn; public class Animal {public void run() {System.out.println("Animal.run...");} }package com.study.jnilearn; public class Cat extends Animal {@Overridepublic void run() {System.out.println(name + " Cat.run...");} }public static void main(String[] args) {Animal cat = new Cat("湯姆");cat.run(); }正如你所看到的那樣,上面這段代碼非常簡(jiǎn)單,有兩個(gè)類 Animal 和 Cat,Animal 類中定義了 run 和 getName 兩個(gè)方法,Cat 繼承自 Animal,并重寫了父類的 run 方法。在 main 方法中,首先定義了一個(gè) Animal 類型的變量 cat,并指向了 Cat 類的實(shí)例對(duì)象,然后調(diào)用了它的 run 方法。在執(zhí)行 new Cat(“湯姆”)這段代碼時(shí),會(huì)先為 Cat 類分配內(nèi)存空間(所分配的內(nèi)存空間大小由 Cat 類的成員變量數(shù)量決定),然后調(diào)用 Cat 的帶參構(gòu)造方法初始化對(duì)象。?cat 是 Animal 類型,但它指向的是 Cat 實(shí)例對(duì)象的引用,而且 Cat 重寫了父類的 run 方法,因?yàn)檎{(diào)用 run 方法時(shí)有多態(tài)存在,所以訪問(wèn)的是 Cat 的 run 而非 Animal 的 run,運(yùn)行后打印的結(jié)果為:湯姆 Cat.run…
如果要調(diào)用父類的 run 方法,只需在 Cat 的 run 方法中調(diào)用 super.run() 即可,相當(dāng)?shù)暮?jiǎn)單。
寫過(guò) C 或 C++ 的同學(xué)應(yīng)該都有一個(gè)很深刻的內(nèi)存管理概念,棧空間和堆空間,棧空間的內(nèi)存大小受操作系統(tǒng)限制,由操作系統(tǒng)自動(dòng)來(lái)管理,速度較快,所以在函數(shù)中定義的局部變量、函數(shù)形參變量都存儲(chǔ)在棧空間。操作系統(tǒng)沒(méi)有限制堆空間的內(nèi)存大小,只受物理內(nèi)存的限制,內(nèi)存需要程序員自己管理。在 C 語(yǔ)言中用 malloc 關(guān)鍵字動(dòng)態(tài)分配的內(nèi)存和在 C++ 中用 new 創(chuàng)建的對(duì)象所分配內(nèi)存都存儲(chǔ)在堆空間,內(nèi)存使用完之后分別用free或delete/delete[]釋放。這里不過(guò)多的討論 C/C++ 內(nèi)存管理方面的知識(shí),有興趣的同學(xué)請(qǐng)自行百度。做 Java 的童鞋眾所周知,寫 Java 程序是不需要手動(dòng)來(lái)管理內(nèi)存的,內(nèi)存管理那些煩鎖的事情全都交由一個(gè)叫 GC 的線程來(lái)管理(當(dāng)一個(gè)對(duì)象沒(méi)有被其它對(duì)象所引用時(shí),該對(duì)象就會(huì)被 GC 釋放)。但我覺(jué)得 Java 內(nèi)部的內(nèi)存管理原理和 C/C++ 是非常相似的,上例中,Animal cat = new Cat(“湯姆”);局部變量 cat 存放在棧空間上,new Cat (“湯姆”);創(chuàng)建的實(shí)例對(duì)象存放在堆空間,返回一個(gè)內(nèi)存地址的引用,存儲(chǔ)在 cat 變量中。這樣就可以通過(guò) cat 變量所指向的引用訪問(wèn) Cat 實(shí)例當(dāng)中所有可見(jiàn)的成員了。
所以創(chuàng)建一個(gè)對(duì)象分為 2 步:
- 為對(duì)象分配內(nèi)存空間
- 初始化對(duì)象(調(diào)用對(duì)象的構(gòu)造方法)
下面通過(guò)一個(gè)示例來(lái)了解在 JNI 中是如何調(diào)用對(duì)象構(gòu)造方法和父類實(shí)例方法的。為了讓示例能清晰的體現(xiàn)構(gòu)造方法和父類實(shí)例方法的調(diào)用流程,定義了 Animal 和 Cat 兩個(gè)類,Animal 定義了一個(gè) String 形參的構(gòu)造方法,一個(gè)成員變量 name、兩個(gè)成員函數(shù) run 和 getName,Cat 繼承自 Animal,并重寫了 run 方法。在 JNI 中實(shí)現(xiàn)創(chuàng)建 Cat 對(duì)象的實(shí)例,調(diào)用 Animal 類的 run 和 getName 方法。代碼如下所示。
// Animal.java package com.study.jnilearn; public class Animal {protected String name;public Animal(String name) {this.name = name;System.out.println("Animal Construct call...");}public String getName() {System.out.println("Animal.getName Call...");return this.name;}public void run() {System.out.println("Animal.run...");} }// Cat.java package com.study.jnilearn; public class Cat extends Animal {public Cat(String name) {super(name);System.out.println("Cat Construct call....");}@Overridepublic String getName() {return "My name is " + this.name;}@Overridepublic void run() {System.out.println(name + " Cat.run...");} }// AccessSuperMethod.java package com.study.jnilearn; public class AccessSuperMethod {public native static void callSuperInstanceMethod(); public static void main(String[] args) {callSuperInstanceMethod();}static {System.loadLibrary("AccessSuperMethod");} }AccessSuperMethod 類是程序的入口,其中定義了一個(gè) native 方法 callSuperInstanceMethod。用 javah 生成的 jni 函數(shù)原型如下。
/* Header for class com_study_jnilearn_AccessSuperMethod */#ifndef _Included_com_study_jnilearn_AccessSuperMethod #define _Included_com_study_jnilearn_AccessSuperMethod #ifdef __cplusplus extern "C" { #endif /** Class: com_study_jnilearn_AccessSuperMethod* Method: callSuperInstanceMethod* Signature: ()V*/ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessSuperMethod_callSuperInstanceMethod(JNIEnv *, jclass);#ifdef __cplusplus } #endif #endif實(shí)現(xiàn) Java_com_study_jnilearn_AccessSuperMethod_callSuperInstanceMethod 函數(shù),如下所示。
/ AccessSuperMethod.c#include "com_study_jnilearn_AccessSuperMethod.h"JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessSuperMethod_callSuperInstanceMethod(JNIEnv *env, jclass cls) {jclass cls_cat;jclass cls_animal;jmethodID mid_cat_init;jmethodID mid_run;jmethodID mid_getName;jstring c_str_name;jobject obj_cat;const char *name = NULL;// 1、獲取Cat類的class引用cls_cat = (*env)->FindClass(env, "com/study/jnilearn/Cat");if (cls_cat == NULL) {return;}// 2、獲取Cat的構(gòu)造方法ID(構(gòu)造方法的名統(tǒng)一為:<init>)mid_cat_init = (*env)->GetMethodID(env, cls_cat, "<init>", "(Ljava/lang/String;)V");if (mid_cat_init == NULL) {return; // 沒(méi)有找到只有一個(gè)參數(shù)為String的構(gòu)造方法}// 3、創(chuàng)建一個(gè)String對(duì)象,作為構(gòu)造方法的參數(shù)c_str_name = (*env)->NewStringUTF(env, "湯姆貓");if (c_str_name == NULL) {return; // 創(chuàng)建字符串失敗(內(nèi)存不夠)}// 4、創(chuàng)建Cat對(duì)象的實(shí)例(調(diào)用對(duì)象的構(gòu)造方法并初始化對(duì)象)obj_cat = (*env)->NewObject(env,cls_cat, mid_cat_init,c_str_name);if (obj_cat == NULL) {return;}//-------------- 5、調(diào)用Cat父類Animal的run和getName方法 --------------cls_animal = (*env)->FindClass(env, "com/study/jnilearn/Animal");if (cls_animal == NULL) {return;}// 例1: 調(diào)用父類的run方法mid_run = (*env)->GetMethodID(env, cls_animal, "run", "()V"); // 獲取父類Animal中run方法的idif (mid_run == NULL) {return;}// 注意:obj_cat是Cat的實(shí)例,cls_animal是Animal的Class引用,mid_run是Animal類中的方法ID(*env)->CallNonvirtualVoidMethod(env, obj_cat, cls_animal, mid_run);// 例2:調(diào)用父類的getName方法// 獲取父類Animal中g(shù)etName方法的idmid_getName = (*env)->GetMethodID(env, cls_animal, "getName", "()Ljava/lang/String;");if (mid_getName == NULL) {return;}c_str_name = (*env)->CallNonvirtualObjectMethod(env, obj_cat, cls_animal, mid_getName);name = (*env)->GetStringUTFChars(env, c_str_name, NULL);printf("In C: Animal Name is %s\n", name);// 釋放從java層獲取到的字符串所分配的內(nèi)存(*env)->ReleaseStringUTFChars(env, c_str_name, name);quit:// 刪除局部引用(jobject或jobject的子類才屬于引用變量),允許VM釋放被局部變量所引用的資源(*env)->DeleteLocalRef(env, cls_cat);(*env)->DeleteLocalRef(env, cls_animal);(*env)->DeleteLocalRef(env, c_str_name);(*env)->DeleteLocalRef(env, obj_cat); }運(yùn)行結(jié)果
代碼講解 - 調(diào)用構(gòu)造方法
調(diào)用構(gòu)造方法和調(diào)用對(duì)象的實(shí)例方法方式是相似的,傳入”< init >”作為方法名查找類的構(gòu)造方法ID,然后調(diào)用JNI函數(shù)NewObject調(diào)用對(duì)象的構(gòu)造函數(shù)初始化對(duì)象。如下代碼所示。
obj_cat = (*env)->NewObject(env,cls_cat,mid_cat_init,c_str_name);上述這段代碼調(diào)用了 JNI 函數(shù) NewObject 創(chuàng)建了 Class 引用的一個(gè)實(shí)例對(duì)象。這個(gè)函數(shù)做了 2 件事情
- 創(chuàng)建一個(gè)未初始化的對(duì)象并分配內(nèi)存空間
- 調(diào)用對(duì)象的構(gòu)造函數(shù)初始化對(duì)象。這兩步也可以分開進(jìn)行,為對(duì)象分配內(nèi)存,然后再初始化對(duì)象,如下代碼所示:
AllocObject 函數(shù)創(chuàng)建的是一個(gè)未初始化的對(duì)象,后面在用這個(gè)對(duì)象之前,必須調(diào)用CallNonvirtualVoidMethod 調(diào)用對(duì)象的構(gòu)造函數(shù)初始化該對(duì)象。而且在使用時(shí)一定要非常小心,確保在一個(gè)對(duì)象上面,構(gòu)造函數(shù)最多被調(diào)用一次。有時(shí),先創(chuàng)建一個(gè)初始化的對(duì)象,然后在合適的時(shí)間再調(diào)用構(gòu)造函數(shù)的方式是很有用的。盡管如此,大部分情況下,應(yīng)該使用 NewObject,盡量避免使用容易出錯(cuò)的 AllocObject/CallNonvirtualVoidMethod 函數(shù)。
代碼講解 - 調(diào)用父類實(shí)例方法
如果一個(gè)方法被定義在父類中,在子類中被覆蓋,也可以調(diào)用父類中的這個(gè)實(shí)例方法。JNI 提供了一系列函數(shù)CallNonvirtualXXXMethod 來(lái)支持調(diào)用各種返回值類型的實(shí)例方法。調(diào)用一個(gè)定義在父類中的實(shí)例方法,須遵循下面的步驟。
使用 GetMethodID 函數(shù)從一個(gè)指向父類的 Class 引用當(dāng)中獲取方法 ID。
cls_animal = (*env)->FindClass(env, "com/study/jnilearn/Animal"); if (cls_animal == NULL) {return; }//例1: 調(diào)用父類的run方法 mid_run = (*env)->GetMethodID(env, cls_animal, "run", "()V"); // 獲取父類Animal中run方法的id if (mid_run == NULL) {return; }傳入子類對(duì)象、父類 Class 引用、父類方法 ID 和參數(shù),并調(diào)用 CallNonvirtualVoidMethod、 CallNonvirtualBooleanMethod、CallNonvirtualIntMethod 等一系列函數(shù)中的一個(gè)。其中CallNonvirtualVoidMethod 也可以被用來(lái)調(diào)用父類的構(gòu)造函數(shù)。
// 注意:obj_cat是Cat的實(shí)例,cls_animal是Animal的Class引用,mid_run是Animal類中的方法ID (*env)->CallNonvirtualVoidMethod(env, obj_cat, cls_animal, mid_run);其實(shí)在開發(fā)當(dāng)中,這種調(diào)用父類實(shí)例方法的情況是很少遇到的,通常在 JAVA 中可以很簡(jiǎn)單地做到:?super.func();但有些特殊需求也可能會(huì)用到,所以知道有這么回事還是很有必要的。
?
JNI 調(diào)用性能測(cè)試及優(yōu)化
在前面幾章我們學(xué)習(xí)到了,在 Java 中聲明一個(gè) native 方法,然后生成本地接口的函數(shù)原型聲明,再用 C/C++ 實(shí)現(xiàn)這些函數(shù),并生成對(duì)應(yīng)平臺(tái)的動(dòng)態(tài)共享庫(kù)放到 Java 程序的類路徑下,最后在 Java 程序中調(diào)用聲明的 native 方法就間接的調(diào)用到了 C/C++ 編寫的函數(shù)了,在 C/C++ 中寫的程序可以避開 JVM 的內(nèi)存開銷過(guò)大的限制、處理高性能的計(jì)算、調(diào)用系統(tǒng)服務(wù)等功能。同時(shí)也學(xué)習(xí)到了在本地代碼中通過(guò) JNI 提供的接口,調(diào)用 Java 程序中的任意方法和對(duì)象的屬性。這是 JNI 提供的一些優(yōu)勢(shì)。但做過(guò) Java 的童鞋應(yīng)該都明白,Java 程序是運(yùn)行在 JVM 上的,所以在 Java 中調(diào)用 C/C++ 或其它語(yǔ)言這種跨語(yǔ)言的接口時(shí),或者說(shuō)在 C/C++ 代碼中通過(guò) JNI 接口訪問(wèn) Java 中對(duì)象的方法或?qū)傩詴r(shí),相比 Java 調(diào)用自已的方法,性能是非常低的!網(wǎng)上有朋友針對(duì) Java 調(diào)用本地接口,Java 調(diào) Java 方法做了一次詳細(xì)的測(cè)試,來(lái)充分說(shuō)明在享受 JNI 給程序帶來(lái)優(yōu)勢(shì)的同時(shí),也要接受其所帶來(lái)的性能開銷,請(qǐng)看下面一組測(cè)試數(shù)據(jù)。
Java 調(diào)用 JNI 空函數(shù)與 Java 調(diào)用 Java 空方法性能測(cè)試。
測(cè)試環(huán)境:JDK1.4.2_19、JDK1.5.0_04 和 JDK1.6.0_14,測(cè)試的重復(fù)次數(shù)都是一億次。測(cè)試結(jié)果的絕對(duì)數(shù)值意義不大,僅供參考。因?yàn)楦鶕?jù) JVM 和機(jī)器性能的不同,測(cè)試所產(chǎn)生的數(shù)值也會(huì)不同,但不管什么機(jī)器和 JVM 應(yīng)該都能反應(yīng)同一個(gè)問(wèn)題,Java 調(diào)用 native 接口,要比 Java 調(diào)用 Java 方法性能要低很多。
Java 調(diào)用 Java 空方法的性能:
| 1.6 | 329ms | 303951367次 |
| 1.5 | 312ms | 320512820次 |
| 1.4 | 312ms | 27233115次 |
Java 調(diào)用 JNI 空函數(shù)的性能:
| 1.6 | 1531ms | 65316786次 |
| 1.5 | 1891ms | 52882072次 |
| 1.4 | 3672ms | 27233115次 |
從上述測(cè)試數(shù)據(jù)可以看出 JDK 版本越高,JNI 調(diào)用的性能也越好。在 JDK1.5 中,僅僅是空方法調(diào)用,JNI 的性能就要比 Java 內(nèi)部調(diào)用慢將近 5 倍,而在 JDK1.4 下更是慢了十多倍。
JNI查找方法ID、字段ID、Class引用性能測(cè)試
當(dāng)我們?cè)诒镜卮a中要訪問(wèn) Java 對(duì)象的字段或調(diào)用它們的方法時(shí),本機(jī)代碼必須調(diào)用 FindClass()、GetFieldID()、GetStaticFieldID、GetMethodID()和 GetStaticMethodID()。對(duì)于 GetFieldID()、GetStaticFieldID、GetMethodID() 和 GetStaticMethodID(),為特定類返回的 ID 不會(huì)在 JVM 進(jìn)程的生存期內(nèi)發(fā)生變化。但是,獲取字段或方法的調(diào)用有時(shí)會(huì)需要在 JVM 中完成大量工作,因?yàn)樽侄魏头椒赡苁菑某愔欣^承而來(lái)的,這會(huì)讓 JVM 向上遍歷類層次結(jié)構(gòu)來(lái)找到它們。由于 ID 對(duì)于特定類是相同的,因此只需要查找一次,然后便可重復(fù)使用。同樣,查找類對(duì)象的開銷也很大,因此也應(yīng)該緩存它們。下面對(duì)調(diào)用 JNI 接口 FindClass 查找 Class、GetFieldID 獲取類的字段 ID 和 GetFieldValue 獲取字段的值的性能做的一個(gè)測(cè)試。緩存表示只調(diào)用一次,不緩存就是每次都調(diào)用相應(yīng)的 JNI 接口:
java.version = 1.6.0_14
- JNI 字段讀取 (緩存Class=false ,緩存字段ID=false) 耗時(shí) : 79172 ms 平均每秒 : 1263072
- JNI 字段讀取 (緩存Class=true ,緩存字段ID=false) 耗時(shí) : 25015 ms 平均每秒 : 3997601
- JNI 字段讀取 (緩存Class=false ,緩存字段ID=true) 耗時(shí) : 50765 ms 平均每秒 : 1969861
- JNI 字段讀取 (緩存Class=true ,緩存字段ID=true) 耗時(shí) : 2125 ms 平均每秒 : 47058823
java.version = 1.5.0_04
- JNI 字段讀取 (緩存Class=false ,緩存字段ID=false) 耗時(shí) : 87109 ms 平均每秒 : 1147987
- JNI 字段讀取 (緩存Class=true ,緩存字段ID=false) 耗時(shí) : 32031 ms 平均每秒 : 3121975
- JNI 字段讀取 (緩存Class=false ,緩存字段ID=true) 耗時(shí) : 51657 ms 平均每秒 : 1935846
- JNI 字段讀取 (緩存Class=true ,緩存字段ID=true) 耗時(shí) : 2187 ms 平均每秒 : 45724737
java.version = 1.4.2_19
- JNI 字段讀取 (緩存Class=false ,緩存字段ID=false) 耗時(shí) : 97500 ms 平均每秒 : 1025641
- JNI 字段讀取 (緩存Class=true ,緩存字段ID=false) 耗時(shí) : 38110 ms 平均每秒 : 2623983
- JNI 字段讀取 (緩存Class=false ,緩存字段ID=true) 耗時(shí) : 55204 ms 平均每秒 : 1811462
- JNI 字段讀取 (緩存Class=true ,緩存字段ID=true) 耗時(shí) : 4187 ms 平均每秒 : 23883448
根據(jù)上面的測(cè)試數(shù)據(jù)得知,查找 class 和 ID (屬性和方法 ID)消耗的時(shí)間比較大。只是讀取字段值的時(shí)間基本上跟上面的 JNI 空方法是一個(gè)數(shù)量級(jí)。而如果每次都根據(jù)名稱查找 class 和 field 的話,性能要下降高達(dá)40倍。讀取一個(gè)字段值的性能在百萬(wàn)級(jí)上,在交互頻繁的 JNI 應(yīng)用中是不能忍受的。 消耗時(shí)間最多的就是查找class,因此在 native 里保存 class 和 member id 是很有必要的。class 和 member id 在一定范圍內(nèi)是穩(wěn)定的,但在動(dòng)態(tài)加載的 class loader 下,保存全局的 class 要么可能失效,要么可能造成無(wú)法卸載classloader,在諸如 OSGI 框架下的 JNI 應(yīng)用還要特別注意這方面的問(wèn)題。在讀取字段值和查找 FieldID 上,JDK1.4 和 1.5、1.6 的差距是非常明顯的。但在最耗時(shí)的查找 class 上,三個(gè)版本沒(méi)有明顯差距。
通過(guò)上面的測(cè)試可以明顯的看出,在調(diào)用 JNI 接口獲取方法 ID、字段 ID 和 Class 引用時(shí),如果沒(méi)用使用緩存的話,性能低至 4 倍。所以在 JNI 開發(fā)中,合理的使用緩存技術(shù)能給程序提高極大的性能。緩存有兩種,分別為使用時(shí)緩存和類靜態(tài)初始化時(shí)緩存,區(qū)別主要在于緩存發(fā)生的時(shí)刻。
使用時(shí)緩存
字段 ID、方法 ID 和 Class 引用在函數(shù)當(dāng)中使用的同時(shí)就緩存起來(lái)。下面看一個(gè)示例:
package com.study.jnilearn;public class AccessCache {private String str = "Hello";public native void accessField(); // 訪問(wèn)str成員變量public native String newString(char[] chars, int len); // 根據(jù)字符數(shù)組和指定長(zhǎng)度創(chuàng)建String對(duì)象public static void main(String[] args) {AccessCache accessCache = new AccessCache();accessCache.nativeMethod();char chars[] = new char[7];chars[0] = '中';chars[1] = '華';chars[2] = '人';chars[3] = '民';chars[4] = '共';chars[5] = '和';chars[6] = '國(guó)';String str = accessCache.newString(chars, 6);System.out.println(str);}static {System.loadLibrary("AccessCache");} }javah 生成的頭文件:com_study_jnilearn_AccessCache.h
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_study_jnilearn_AccessCache */ #ifndef _Included_com_study_jnilearn_AccessCache #define _Included_com_study_jnilearn_AccessCache #ifdef __cplusplus extern "C" { #endif /** Class: com_study_jnilearn_AccessCache* Method: accessField* Signature: ()V*/ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_accessField(JNIEnv *, jobject);/** Class: com_study_jnilearn_AccessCache* Method: newString* Signature: ([CI)Ljava/lang/String;*/ JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString(JNIEnv *, jobject, jcharArray, jint);#ifdef __cplusplus } #endif #endif實(shí)現(xiàn)頭文件中的函數(shù):AccessCache.c
// AccessCache.c #include "com_study_jnilearn_AccessCache.h"JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_accessField(JNIEnv *env, jobject obj) {// 第一次訪問(wèn)時(shí)將字段存到內(nèi)存數(shù)據(jù)區(qū),直到程序結(jié)束才會(huì)釋放,可以起到緩存的作用static jfieldID fid_str = NULL;jclass cls_AccessCache;jstring j_str;const char *c_str;cls_AccessCache = (*env)->GetObjectClass(env, obj); // 獲取該對(duì)象的Class引用if (cls_AccessCache == NULL) {return;}// 先判斷字段ID之前是否已經(jīng)緩存過(guò),如果已經(jīng)緩存過(guò)則不進(jìn)行查找if (fid_str == NULL) {fid_str = (*env)->GetFieldID(env,cls_AccessCache,"str","Ljava/lang/String;");// 再次判斷是否找到該類的str字段if (fid_str == NULL) {return;}}j_str = (*env)->GetObjectField(env, obj, fid_str); // 獲取字段的值c_str = (*env)->GetStringUTFChars(env, j_str, NULL);if (c_str == NULL) {return; // 內(nèi)存不夠}printf("In C:\n str = \"%s\"\n", c_str);(*env)->ReleaseStringUTFChars(env, j_str, c_str); // 釋放從從JVM新分配字符串的內(nèi)存空間// 修改字段的值j_str = (*env)->NewStringUTF(env, "12345");if (j_str == NULL) {return;}(*env)->SetObjectField(env, obj, fid_str, j_str);// 釋放本地引用(*env)->DeleteLocalRef(env,cls_AccessCache);(*env)->DeleteLocalRef(env,j_str); }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ò)誤,這里做為一個(gè)反面教材提醒大家,下面會(huì)說(shuō)到。if (cls_string == NULL) {cls_string = (*env)->FindClass(env, "java/lang/String");if (cls_string == NULL) {return NULL;}}// 緩存String的構(gòu)造方法IDif (cid_string == NULL) {cid_string = (*env)->GetMethodID(env, cls_string, "<init>", "([C)V");if (cid_string == NULL) {return NULL;}}printf("In C array Len: %d\n", len);// 創(chuàng)建一個(gè)字符數(shù)組elemArray = (*env)->NewCharArray(env, len);if (elemArray == NULL) {return NULL;}// 獲取數(shù)組的指針引用,注意:不能直接將jcharArray作為SetCharArrayRegion函數(shù)最后一個(gè)參數(shù)chars = (*env)->GetCharArrayElements(env, j_char_arr,NULL);if (chars == NULL) {return NULL;}// 將Java字符數(shù)組中的內(nèi)容復(fù)制指定長(zhǎng)度到新的字符數(shù)組中(*env)->SetCharArrayRegion(env, elemArray, 0, len, chars);// 調(diào)用String對(duì)象的構(gòu)造方法,創(chuàng)建一個(gè)指定字符數(shù)組為內(nèi)容的String對(duì)象j_str = (*env)->NewObject(env, cls_string, cid_string, elemArray);// 釋放本地引用(*env)->DeleteLocalRef(env, elemArray);return j_str; }例1、在 Java_com_study_jnilearn_AccessCache_accessField 函數(shù)中定義了一個(gè)靜態(tài)變量fid_str用于存儲(chǔ)字段的 ID,每次調(diào)用函數(shù)的時(shí)候
static jfieldID fid_str = NULL;在代碼段
// 先判斷字段ID之前是否已經(jīng)緩存過(guò),如果已經(jīng)緩存過(guò)則不進(jìn)行查找if (fid_str == NULL) {fid_str = (*env)->GetFieldID(env,cls_AccessCache,"str","Ljava/lang/String;");// 再次判斷是否找到該類的str字段if (fid_str == NULL) {return;}}判斷字段 ID 是否已經(jīng)緩存,如果沒(méi)有先取出來(lái)存到fid_str中,下次再調(diào)用的時(shí)候該變量已經(jīng)有值了,不用再去JVM中獲取,起到了緩存的作用。
在 Java_com_study_jnilearn_AccessCache_newString 函數(shù)中定義了兩個(gè)變量cls_string和cid_string,分別用于存儲(chǔ) java.lang.String 類的 Class 引用和 String 的構(gòu)造方法 ID。在使用前會(huì)先判斷是否已經(jīng)緩存過(guò),如果沒(méi)有則調(diào)用 JNI 的接口從 JVM 中獲取 String 的 Class 引用和構(gòu)造方法 ID 存儲(chǔ)到靜態(tài)變量當(dāng)中。下次再調(diào)用該函數(shù)時(shí)就可以直接使用,不需要再去找一次了,也達(dá)到了緩存的效果,大家第一反映都會(huì)這么認(rèn)為。但是請(qǐng)注意:cls_string是一個(gè)局部引用,與方法和字段 ID 不一樣,局部引用在函數(shù)結(jié)束后會(huì)被 JVM 自動(dòng)釋放掉,這時(shí)cls_string成為了一個(gè)野針對(duì)(指向的內(nèi)存空間已被釋放,但變量的值仍然是被釋放后的內(nèi)存地址,不為 NULL),當(dāng)下次再調(diào)用 Java_com_xxxx_newString 這個(gè)函數(shù)的時(shí)候,會(huì)試圖訪問(wèn)一個(gè)無(wú)效的局部引用,從而導(dǎo)致非法的內(nèi)存訪問(wèn)造成程序崩潰。所以在函數(shù)內(nèi)用 static 緩存局部引用這種方式是錯(cuò)誤的。下篇文章會(huì)介紹局部引用和全局引用,利用全局引用來(lái)防止這種問(wèn)題,請(qǐng)關(guān)注。
類靜態(tài)初始化緩存
在調(diào)用一個(gè)類的方法或?qū)傩灾?#xff0c;Java 虛擬機(jī)會(huì)先檢查該類是否已經(jīng)加載到內(nèi)存當(dāng)中,如果沒(méi)有則會(huì)先加載,然后緊接著會(huì)調(diào)用該類的靜態(tài)初始化代碼塊,所以在靜態(tài)初始化該類的過(guò)程當(dāng)中計(jì)算并緩存該類當(dāng)中的字段 ID 和方法 ID 也是個(gè)不錯(cuò)的選擇。下面看一個(gè)示例:
package com.study.jnilearn;public class AccessCache {public static native void initIDs(); public native void nativeMethod();public void callback() {System.out.println("AccessCache.callback invoked!");}public static void main(String[] args) {AccessCache accessCache = new AccessCache();accessCache.nativeMethod();}static {System.loadLibrary("AccessCache");initIDs();} } /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_study_jnilearn_AccessCache */ #ifndef _Included_com_study_jnilearn_AccessCache #define _Included_com_study_jnilearn_AccessCache #ifdef __cplusplus extern "C" { #endif /** Class: com_study_jnilearn_AccessCache* Method: initIDs* Signature: ()V*/ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_initIDs(JNIEnv *, jclass);/** Class: com_study_jnilearn_AccessCache* Method: nativeMethod* Signature: ()V*/ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_nativeMethod(JNIEnv *, jobject);#ifdef __cplusplus } #endif #endif // AccessCache.c#include "com_study_jnilearn_AccessCache.h"jmethodID MID_AccessCache_callback;JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_initIDs (JNIEnv *env, jclass cls) {printf("initIDs called!!!\n");MID_AccessCache_callback = (*env)->GetMethodID(env,cls,"callback","()V"); }JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_nativeMethod (JNIEnv *env, jobject obj) {printf("In C Java_com_study_jnilearn_AccessCache_nativeMethod called!!!\n");(*env)->CallVoidMethod(env, obj, MID_AccessCache_callback); }JVM 加載 AccessCache.class 到內(nèi)存當(dāng)中之后,會(huì)調(diào)用該類的靜態(tài)初始化代碼塊,即 static 代碼塊,先調(diào)用System.loadLibrary 加載動(dòng)態(tài)庫(kù)到 JVM 中,緊接著調(diào)用 native 方法 initIDs,會(huì)調(diào)用用到本地函數(shù)Java_com_study_jnilearn_AccessCache_initIDs,在該函數(shù)中獲取需要緩存的 ID,然后存入全局變量當(dāng)中。下次需要用到這些 ID 的時(shí)候,直接使用全局變量當(dāng)中的即可,調(diào)用 Java 的 callback 函數(shù)。
(*env)->CallVoidMethod(env, obj, MID_AccessCache_callback);兩種緩存方式比較
如果在寫 JNI 接口時(shí),不能控制方法和字段所在類的源碼的話,用使用時(shí)緩存比較合理。但比起類靜態(tài)初始化時(shí)緩存來(lái)說(shuō),用使用時(shí)緩存有一些缺點(diǎn):
- 使用前,每次都需要檢查是否已經(jīng)緩存該 ID 或 Class 引用
- 如果在用使用時(shí)緩存的 ID,要注意只要本地代碼依賴于這個(gè) ID 的值,那么這個(gè)類就不會(huì)被 unload。另外一方面,如果緩存發(fā)生在靜態(tài)初始化時(shí),當(dāng)類被 unload 或 reload 時(shí),ID 會(huì)被重新計(jì)算。因?yàn)?#xff0c;盡量在類靜態(tài)初始化時(shí)就緩存字段 ID、方法 ID 和類的 Class 引用。
?
JNI 局部引用、全局引用和弱全局引用
這篇文章比較偏理論,詳細(xì)介紹了在編寫本地代碼時(shí)三種引用的使用場(chǎng)景和注意事項(xiàng)。可能看起來(lái)有點(diǎn)枯燥,但引用是在 JNI 中最容易出錯(cuò)的一個(gè)點(diǎn),如果使用不當(dāng),容易使程序造成內(nèi)存溢出,程序崩潰等現(xiàn)象。《Android JNI局部引用表溢出》這篇文章是一個(gè) JNI 引用使用不當(dāng)造成引用表溢出,最終導(dǎo)致程序崩潰的例子。建議看完這篇文章之后,再去看。
做 Java 的朋友都知道,在編碼的過(guò)程當(dāng)中,內(nèi)存管理這一塊完全是透明的。new 一個(gè)類的實(shí)例時(shí),只知道創(chuàng)建完這個(gè)類的實(shí)例之后,會(huì)返回這個(gè)實(shí)例的一個(gè)引用,然后就可以拿著這個(gè)引用訪問(wèn)它的所有數(shù)據(jù)成員了(屬性、方法)。完全不用管 JVM 內(nèi)部是怎么實(shí)現(xiàn)的,如何為新創(chuàng)建的對(duì)象來(lái)申請(qǐng)內(nèi)存,也不用管對(duì)象使用完之后內(nèi)存是怎么釋放的,只需知道有一個(gè)垃圾回器在幫忙管理這些事情就 OK 的了。有經(jīng)驗(yàn)的朋友也許知道啟動(dòng)一個(gè) Java 程序,如果沒(méi)有手動(dòng)創(chuàng)建其它線程,默認(rèn)會(huì)有兩個(gè)線程在跑,一個(gè)是 main 線程,另一個(gè)就是 GC 線程(負(fù)責(zé)將一些不再使用的對(duì)象回收)。如果你曾經(jīng)是做 Java 的然后轉(zhuǎn)去做 C++,會(huì)感覺(jué)很不習(xí)慣,在 C++ 中 new 一個(gè)對(duì)象,使用完了還要做一次 delete 操作,malloc 一次同樣也要調(diào)用 free 來(lái)釋放相應(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 的人聽(tīng)到這些肯定會(huì)偷樂(lè)了,咱寫 Java 的時(shí)候這些都不用管,全都交給 GC 就萬(wàn)事無(wú)優(yōu)了。手動(dòng)管理內(nèi)存雖然麻煩,而且需要特別細(xì)心,一不小心就有可能造成內(nèi)存泄露和野指針訪問(wèn)等程序致命的問(wèn)題,但凡事都有利弊,手動(dòng)申請(qǐng)和釋放內(nèi)存對(duì)程序的掌握比較靈活,不會(huì)受到平臺(tái)的限制。比如我們寫Android程序的時(shí)候,內(nèi)存使用就受Dalivk虛擬機(jī)的限制,從最初版本的16~24M,到后來(lái)的 32M 到 64M,可能隨著以后移動(dòng)設(shè)備物理內(nèi)存的不大擴(kuò)大,后面的 Android 版本內(nèi)存限制可能也會(huì)隨著提高。但在 C/C++ 這層,就完全不受虛擬機(jī)的限制了。比如要在 Android 中要存儲(chǔ)一張超高清的圖片,剛好這張圖片的大小超過(guò)了 Dalivk 虛擬機(jī)對(duì)每個(gè)應(yīng)用的內(nèi)存大小限制,Java 此時(shí)就顯得無(wú)能為力了,但在C/C++ 看來(lái)就是小菜一碟了,malloc(1024102450)。C/C++ 程序員得意的說(shuō)道,Java 不是說(shuō)是一門純面象對(duì)象的語(yǔ)言嗎,所以除了基本數(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)為是不再使用,并且可以被回收的呢?我們來(lái)看下面二張圖。(注:圖摘自博主郭霖的《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ì)象開始檢查,所有它可以訪問(wèn)到的對(duì)象就說(shuō)明還在使用當(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)單流程。
上面說(shuō)的廢話好像有點(diǎn)多哈,下面進(jìn)入正題。通過(guò)上面的討論,大家都知道,如果一個(gè) Java 對(duì)象沒(méi)有被其它成員變量或靜態(tài)變量所引用的話,就隨時(shí)有可能會(huì)被 GC 回收掉。所以我們?cè)诰帉懕镜卮a時(shí),要注意從 JVM 中獲取到的引用在使用時(shí)被 GC 回收的可能性。由于本地代碼不能直接通過(guò)引用操作 JVM 內(nèi)部的數(shù)據(jù)結(jié)構(gòu),要進(jìn)行這些操作必須調(diào)用相應(yīng)的 JNI 接口來(lái)間接操作所引用的數(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)槲覀冎煌ㄟ^(guò) 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/C++ 中想要訪問(wèn) Java 中對(duì)象的屬性和方法時(shí),是不能夠直接操作 JVM 內(nèi)部 Java 對(duì)象的數(shù)據(jù)結(jié)構(gòu)的。想要在 C/C++ 中正確的訪問(wèn) Java 的數(shù)據(jù)結(jié)構(gòu),JVM 就必須有一套規(guī)則來(lái)約束 C/C++ 與 Java 互相訪問(wèn)的機(jī)制,所以才有了 JNI 規(guī)范,JNI 規(guī)范定義了一系列接口,任何實(shí)現(xiàn)了這套 JNI 接口的 Java 虛擬機(jī),C/C++ 就可以通過(guò)調(diào)用這一系列接口來(lái)間接的訪問(wèn) 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ū)別如下:
局部引用
通過(guò) 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)
jclass cls_string = (*env)->FindClass(env, "java/lang/String"); jcharArray charArr = (*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); // 通過(guò)NewLocalRef函數(shù)創(chuàng)建 ...全局引用
調(diào)用 NewGlobalRef 基于局部引用創(chuàng)建,會(huì)阻 GC 回收所引用的對(duì)象。可以跨方法、跨線程使用。JVM 不會(huì)自動(dòng)釋放,必須調(diào)用 DeleteGlobalRef 手動(dòng)釋放。(*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); }弱全局引用
調(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 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); }局部引用
局部引用也稱本地引用,通常是在函數(shù)中創(chuàng)建并使用。會(huì)阻止 GC 回收所引用的對(duì)象。比如,調(diào)用 NewObject 接口創(chuàng)建一個(gè)新的對(duì)象實(shí)例并返回一個(gè)對(duì)這個(gè)對(duì)象的局部引用。局部引用只有在創(chuàng)建它的本地方法返回前有效,本地方法返回到 Java 層之后,如果 Java 層沒(méi)有對(duì)返回的局部引用使用的話,局部引用就會(huì)被 JVM 自動(dòng)釋放。你可能會(huì)為了提高程序的性能,在函數(shù)中將局部引用存儲(chǔ)在靜態(tài)變量中緩存起來(lái),供下次調(diào)用時(shí)使用。這種方式是錯(cuò)誤的,因?yàn)楹瘮?shù)返回后局部引很可能馬上就會(huì)被釋放掉,靜態(tài)變量中存儲(chǔ)的就是一個(gè)被釋放后的內(nèi)存地址,成了一個(gè)野針對(duì),下次再使用的時(shí)候就會(huì)造成非法地址的訪問(wèn),使程序崩潰。請(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) {return NULL;}}// 緩存String的構(gòu)造方法IDif (cid_string == NULL) {cid_string = (*env)->GetMethodID(env, cls_string, "<init>", "([C)V");if (cid_string == NULL) {return NULL;}}//省略額外的代碼.......elemArray = (*env)->NewCharArray(env, len);// ....j_str = (*env)->NewObject(env, cls_string, cid_string, elemArray);// 釋放局部引用(*env)->DeleteLocalRef(env, elemArray);return j_str; }上面代碼中,我們省略了和我們討論無(wú)關(guān)的代碼。因?yàn)?FindClass 返回一個(gè)對(duì) java.lang.String 對(duì)象的局部引用,上面代碼中緩存 cls_string 做法是錯(cuò)誤的。假設(shè)一個(gè)本地方法 C.f 調(diào)用了 newString。
JNIEXPORT jstring JNICALLJava_C_f(JNIEnv *env, jobject this){char *c_str = ...;...return newString(c_str); }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è)野指針,再訪問(wèn)這個(gè)指針的引用時(shí),會(huì)導(dǎo)致因非法的內(nèi)存訪問(wèn)造成程序崩潰。
... ... = C.f(); // 第一次調(diào)是OK的 ... = C.f(); // 第二次調(diào)用時(shí),訪問(wèn)的是一個(gè)無(wú)效的引用. ...釋放局部引用
釋放一個(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)心局部引用的釋放問(wèn)題,函數(shù)被調(diào)用完成后,JVM 會(huì)自動(dòng)釋放函數(shù)中創(chuàng)建的所有局部引用。盡管如此,以下幾種情況下,為了避免內(nèi)存溢出,我們應(yīng)該手動(dòng)釋放局部引用。
JNI 會(huì)將創(chuàng)建的局部引用都存儲(chǔ)在一個(gè)局部引用表中,如果這個(gè)表超過(guò)了最大容量限制,就會(huì)造成局部引用表溢出,使程序崩潰。經(jīng)測(cè)試,Android 上的 JNI 局部引用表最大數(shù)量是 512 個(gè)。當(dāng)我們?cè)趯?shí)現(xiàn)一個(gè)本地方法時(shí),可能需要?jiǎng)?chuàng)建大量的局部引用,如果沒(méi)有及時(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); // 使用完成之后馬上釋放 }在編寫 JNI 工具函數(shù)時(shí),工具函數(shù)在程序當(dāng)中是公用的,被誰(shuí)調(diào)用你是不知道的。上面 newString 這個(gè)函數(shù)演示了怎么樣在工具函數(shù)中使用完局部引用后,調(diào)用 DeleteLocalRef 刪除。不這樣做的話,每次調(diào)用 newString 之后,都會(huì)遺留兩個(gè)引用占用空間(elemArray和cls_string,cls_string 不用 static 緩存的情況下)。
如果你的本地函數(shù)不會(huì)返回。比如一個(gè)接收消息的函數(shù),里面有一個(gè)死循環(huán),用于等待別人發(fā)送消息過(guò)來(lái)while(true) { if (有新的消息) { 處理之。。。。} else { 等待新的消息。。。}}。如果在消息循環(huán)當(dāng)中創(chuàng)建的引用你不顯示刪除,很快將會(huì)造成 JVM 局部引用表溢出。
局部引用會(huì)阻止所引用的對(duì)象被 GC 回收。比如你寫的一個(gè)本地函數(shù)中剛開始需要訪問(wèn)一個(gè)大對(duì)象,因此一開始就創(chuàng)建了一個(gè)對(duì)這個(gè)對(duì)象的引用,但在函數(shù)返回前會(huì)有一個(gè)大量的非常復(fù)雜的計(jì)算過(guò)程,而在這個(gè)計(jì)算過(guò)程當(dāng)中是不需要前面創(chuàng)建的那個(gè)大對(duì)象的引用的。但是,在計(jì)算的過(guò)程當(dāng)中,如果這個(gè)大對(duì)象的引用還沒(méi)有被釋放的話,會(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ì)算過(guò)程 */return; /* 計(jì)算完成之后,函數(shù)返回之前所有引用都已經(jīng)釋放 */ }管理局部引用
JNI 提供了一系列函數(shù)來(lái)管理局部引用的生命周期。這些函數(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)建更多的引用,可以通過(guò)調(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)建的局部引用超過(guò)了本地引用的最大容量,在運(yùn)行時(shí) JVM 會(huì)調(diào)用 FatalError 函數(shù)使程序強(qiáng)制退出。在開發(fā)過(guò)程當(dāng)中,可以為 JVM 添加-verbose:jni參數(shù),在編譯的時(shí)如果發(fā)現(xiàn)本地代碼在試圖申請(qǐng)過(guò)多的引用時(shí),會(huì)打印警告信息提示我們要注意。在下面的代碼中,遍歷數(shù)組時(shí)會(huì)獲取每個(gè)元素的引用,使用完了之后不手動(dòng)刪除,不考慮內(nèi)存因素的情況下,它可以為這種創(chuàng)建大量的局部引用提供足夠的空間。由于沒(méi)有及時(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字符串/*這里沒(méi)有刪除在for中臨時(shí)創(chuàng)建的局部引用*/ }另外,除了 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); }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é)銷毀棧中所有的引用。這樣一來(lái),Push/PopLocalFrame 函數(shù)對(duì)提供了對(duì)局部引用生命周期更方便的管理,而不需要時(shí)刻關(guān)注獲取一個(gè)引用后,再調(diào)用 DeleteLocalRef 來(lái)釋放引用。在上面的例子中,如果在處理 jstr 的過(guò)程當(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); // 銷毀局部引用棧前返回指定的引用 }還要注意的一個(gè)問(wèn)題是,局部引用不能跨線程使用,只在創(chuàng)建它的線程有效。不要試圖在一個(gè)線程中創(chuàng)建局部引用并存儲(chǔ)到全局引用中,然后在另外一個(gè)線程中使用。
全局引用
全局引用可以跨方法、跨線程使用,直到它被手動(dòng)釋放才會(huì)失效。同局部引用一樣,也會(huì)阻止它所引用的對(duì)象被 GC 回收。與局部引用創(chuàng)建方式不同的是,只能通過(guò) 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) {return NULL;}// 將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) {return NULL;}}// ....return jstr; }弱全局引用
弱全局引用使用 NewGlobalWeakRef 創(chuàng)建,使用 DeleteGlobalWeakRef 釋放。下面簡(jiǎn)稱弱引用。與全局引用類似,弱引用可以跨方法、線程使用。但與全局引用很重要不同的一點(diǎn)是,弱引用不會(huì)阻止 GC 回收它引用的對(duì)象。在newString 這個(gè)函數(shù)中,我們也可以使用弱引用來(lái)存儲(chǔ) String 的 Class 引用,因?yàn)?java.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 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; /* 沒(méi)有找到mypkg/MyCls2這個(gè)類 */}myCls2 = NewWeakGlobalRef(env, myCls2Local);if (myCls2 == NULL){return; /* 內(nèi)存溢出 */}}... /* 使用myCls2的引用 */ }我們假設(shè) MyCls 和 MyCls2 有相同的生命周期(例如,他們可能被相同的類加載器加載),因?yàn)槿跻玫拇嬖?#xff0c;我們不必?fù)?dān)心 MyCls 和它所在的本地代碼在被使用時(shí),MyCls2 這個(gè)類出現(xiàn)先被 unload,后來(lái)又會(huì) preload 的情況。當(dāng)然,如果真的發(fā)生這種情況時(shí)(MyCls 和 MyCls2 此時(shí)的生命周期不同),我們?cè)谑褂萌跻脮r(shí),必須先檢查緩存過(guò)的弱引用是指向活動(dòng)的類對(duì)象,還是指向一個(gè)已經(jīng)被 GC 給 unload 的類對(duì)象。下面馬上告訴你怎樣檢查弱引用是否活動(dòng),即引用的比較。
引用比較
給定兩個(gè)引用(不管是全局、局部還是弱全局引用),我們只需要調(diào)用 IsSameObject 來(lái)判斷它們兩個(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?來(lái)判斷 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);在上面的 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)建大量而沒(méi)有被立即回收的引用很可能就會(huì)導(dǎo)致內(nèi)存溢出。 ??? 當(dāng)我們的本地代碼不再需要一個(gè)全局引用時(shí),應(yīng)該馬上調(diào)用 DeleteGlobalRef 來(lái)釋放它。如果不手動(dòng)調(diào)用這個(gè)函數(shù),即使這個(gè)對(duì)象已經(jīng)沒(méi)用了,JVM 也不會(huì)回收這個(gè)全局引用所指向的對(duì)象。 ???? 同樣,當(dāng)我們的本地代碼不再需要一個(gè)弱全局引用時(shí),也應(yīng)該調(diào)用 DeleteWeakGlobalRef 來(lái)釋放它,如果不手動(dòng)調(diào)用這個(gè)函數(shù)來(lái)釋放所指向的對(duì)象,JVM 仍會(huì)回收弱引用所指向的對(duì)象,但弱引用本身在引用表中所占的內(nèi)存永遠(yuǎn)也不會(huì)被回收。
管理引用的規(guī)則
前面對(duì)三種引用已做了一個(gè)全面的介紹,下面來(lái)總結(jié)一下引用的管理規(guī)則和使用時(shí)的一些注意事項(xiàng),使用好引用的目的就是為了減少內(nèi)存使用和對(duì)象被引用保持而不能釋放,造成內(nèi)存浪費(fèi)。所以在開發(fā)當(dāng)中要特別小心!
通常情況下,有兩種本地代碼使用引用時(shí)要注意:
- 直接實(shí)現(xiàn)Java層聲明的native函數(shù)的本地代碼 當(dāng)編寫這類本地代碼時(shí),要當(dāng)心不要造成全局引用和弱引用的累加,因?yàn)楸镜胤椒▓?zhí)行完畢后,這兩種引用不會(huì)被自動(dòng)釋放。
- 被用在任何環(huán)境下的工具函數(shù)。例如:方法調(diào)用、屬性訪問(wèn)和異常處理的工具函數(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ī)則:
- 一個(gè)返回值為基本類型的工具函數(shù)被調(diào)用時(shí),它決不能造成局部、全局、弱全局引用被回收的累加。
- 當(dāng)一個(gè)返回值為引用類型的工具函數(shù)被調(diào)用時(shí),它除了返回的引用以外,它決不能造成其它局部、全局、弱引用的累加。
對(duì)于工具函數(shù)來(lái)說(shuō),為了使用緩存技術(shù)而創(chuàng)建一些全局引用或者弱全局引用是正常的。如果一個(gè)工具函數(shù)返回的是一個(gè)引用,我們應(yīng)該寫好注釋詳細(xì)說(shuō)明返回引用的類型,以便于使用者更好的管理它們。下面的代碼中,頻繁地調(diào)用工具函數(shù) GetInfoString,我們需要知道 GetInfoString 返回引用的類型是什么,以便于每次使用完成后調(diào)用相應(yīng)的 JNI 函數(shù)來(lái)釋放掉它。
while (JNI_TRUE) {jstring infoString = GetInfoString(info);... /* 處理infoString */??? /* 使用完成之后,調(diào)用DeleteLocalRef、DeleteGlobalRef、DeleteWeakGlobalRef哪一個(gè)函數(shù)來(lái)釋放這個(gè)引用呢?*/ }函數(shù) NewLocalRef 有時(shí)被用來(lái)確保一個(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); }... return result; }在管理局部引用的生命周期中,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 語(yǔ)句出現(xiàn)的地方)都要調(diào)用 PopLocalFrame。在下面的代碼中,對(duì) PushLocalFrame 的調(diào)用只有一次,但調(diào)用 PopLocalFrame 確有多次,當(dāng)然你也可以使用 goto 語(yǔ)句來(lái)統(tǒng)一處理。
jobject f(JNIEnv *env, ...) {jobject result;if ((*env)->PushLocalFrame(env, 10) < 0){/* 調(diào)用PushLocalFrame獲取10個(gè)局部引用失敗,不需要調(diào)用PopLocalFrame */return NULL;}...result = ...; // 創(chuàng)建局部引用resultif (...){/* 返回前先彈出棧頂?shù)膄rame */result = (*env)->PopLocalFrame(env, result);return result;}...result = (*env)->PopLocalFrame(env, result);/* 正常返回 */return result; }上面的代碼同樣演示了函數(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)中。
?
編譯C文件:
gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -fPIC -shared AccessSuperMethod.c -o libAccessSuperMethod.so
https://blog.csdn.net/xyang81/article/details/45770551
https://blog.csdn.net/xyang81/article/details/44873769
轉(zhuǎn)載于:https://www.cnblogs.com/EMH899/p/10800644.html
總結(jié)
- 上一篇: shell学习(12)- jq
- 下一篇: 【C】strcpy()需谨慎使用;