日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

jni2

發(fā)布時間:2023/12/18 编程问答 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 jni2 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

JNI 調用構造方法和父類實例方法

在前面我們學習到了在 Native 層如何調用 Java 靜態(tài)方法和實例方法,其中調用實例方法的示例代碼中也提到了調用構造函數(shù)來實始化一個對象,但沒有詳細介紹,一帶而過了。還沒有閱讀過的同學請移步《JNI——C/C++ 訪問 Java 實例方法和靜態(tài)方法》閱讀。這章詳細來介紹下初始一個對象的兩種方式,以及如何調用子類對象重寫的父類實例方法。

構造方法和父類實例方法

我們先回過一下,在 Java 中實例化一個對象和調用父類實例方法的流程。先看一段代碼:

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(); }

正如你所看到的那樣,上面這段代碼非常簡單,有兩個類 Animal 和 Cat,Animal 類中定義了 run 和 getName 兩個方法,Cat 繼承自 Animal,并重寫了父類的 run 方法。在 main 方法中,首先定義了一個 Animal 類型的變量 cat,并指向了 Cat 類的實例對象,然后調用了它的 run 方法。在執(zhí)行 new Cat(“湯姆”)這段代碼時,會先為 Cat 類分配內存空間(所分配的內存空間大小由 Cat 類的成員變量數(shù)量決定),然后調用 Cat 的帶參構造方法初始化對象。?cat 是 Animal 類型,但它指向的是 Cat 實例對象的引用,而且 Cat 重寫了父類的 run 方法,因為調用 run 方法時有多態(tài)存在,所以訪問的是 Cat 的 run 而非 Animal 的 run,運行后打印的結果為:湯姆 Cat.run…

如果要調用父類的 run 方法,只需在 Cat 的 run 方法中調用 super.run() 即可,相當?shù)暮唵巍?/p>

寫過 C 或 C++ 的同學應該都有一個很深刻的內存管理概念,棧空間和堆空間,棧空間的內存大小受操作系統(tǒng)限制,由操作系統(tǒng)自動來管理,速度較快,所以在函數(shù)中定義的局部變量、函數(shù)形參變量都存儲在棧空間。操作系統(tǒng)沒有限制堆空間的內存大小,只受物理內存的限制,內存需要程序員自己管理。在 C 語言中用 malloc 關鍵字動態(tài)分配的內存和在 C++ 中用 new 創(chuàng)建的對象所分配內存都存儲在堆空間,內存使用完之后分別用free或delete/delete[]釋放。這里不過多的討論 C/C++ 內存管理方面的知識,有興趣的同學請自行百度。做 Java 的童鞋眾所周知,寫 Java 程序是不需要手動來管理內存的,內存管理那些煩鎖的事情全都交由一個叫 GC 的線程來管理(當一個對象沒有被其它對象所引用時,該對象就會被 GC 釋放)。但我覺得 Java 內部的內存管理原理和 C/C++ 是非常相似的,上例中,Animal cat = new Cat(“湯姆”);局部變量 cat 存放在棧空間上,new Cat (“湯姆”);創(chuàng)建的實例對象存放在堆空間,返回一個內存地址的引用,存儲在 cat 變量中。這樣就可以通過 cat 變量所指向的引用訪問 Cat 實例當中所有可見的成員了。

所以創(chuàng)建一個對象分為 2 步:

  • 為對象分配內存空間
  • 初始化對象(調用對象的構造方法)

下面通過一個示例來了解在 JNI 中是如何調用對象構造方法和父類實例方法的。為了讓示例能清晰的體現(xiàn)構造方法和父類實例方法的調用流程,定義了 Animal 和 Cat 兩個類,Animal 定義了一個 String 形參的構造方法,一個成員變量 name、兩個成員函數(shù) run 和 getName,Cat 繼承自 Animal,并重寫了 run 方法。在 JNI 中實現(xiàn)創(chuàng)建 Cat 對象的實例,調用 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 類是程序的入口,其中定義了一個 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

實現(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的構造方法ID(構造方法的名統(tǒng)一為:<init>)mid_cat_init = (*env)->GetMethodID(env, cls_cat, "<init>", "(Ljava/lang/String;)V");if (mid_cat_init == NULL) {return; // 沒有找到只有一個參數(shù)為String的構造方法}// 3、創(chuàng)建一個String對象,作為構造方法的參數(shù)c_str_name = (*env)->NewStringUTF(env, "湯姆貓");if (c_str_name == NULL) {return; // 創(chuàng)建字符串失敗(內存不夠)}// 4、創(chuàng)建Cat對象的實例(調用對象的構造方法并初始化對象)obj_cat = (*env)->NewObject(env,cls_cat, mid_cat_init,c_str_name);if (obj_cat == NULL) {return;}//-------------- 5、調用Cat父類Animal的run和getName方法 --------------cls_animal = (*env)->FindClass(env, "com/study/jnilearn/Animal");if (cls_animal == NULL) {return;}// 例1: 調用父類的run方法mid_run = (*env)->GetMethodID(env, cls_animal, "run", "()V"); // 獲取父類Animal中run方法的idif (mid_run == NULL) {return;}// 注意:obj_cat是Cat的實例,cls_animal是Animal的Class引用,mid_run是Animal類中的方法ID(*env)->CallNonvirtualVoidMethod(env, obj_cat, cls_animal, mid_run);// 例2:調用父類的getName方法// 獲取父類Animal中getName方法的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層獲取到的字符串所分配的內存(*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); }

運行結果

代碼講解 - 調用構造方法

調用構造方法和調用對象的實例方法方式是相似的,傳入”< init >”作為方法名查找類的構造方法ID,然后調用JNI函數(shù)NewObject調用對象的構造函數(shù)初始化對象。如下代碼所示。

obj_cat = (*env)->NewObject(env,cls_cat,mid_cat_init,c_str_name);

上述這段代碼調用了 JNI 函數(shù) NewObject 創(chuàng)建了 Class 引用的一個實例對象。這個函數(shù)做了 2 件事情

  • 創(chuàng)建一個未初始化的對象并分配內存空間
  • 調用對象的構造函數(shù)初始化對象。這兩步也可以分開進行,為對象分配內存,然后再初始化對象,如下代碼所示:
// 1、創(chuàng)建一個未初始化的對象,并分配內存obj_cat = (*env)->AllocObject(env, cls_cat);if (obj_cat) {// 2、調用對象的構造函數(shù)初始化對象(*env)->CallNonvirtualVoidMethod(env,obj_cat, cls_cat, mid_cat_init, c_str_name);if ((*env)->ExceptionCheck(env)) { // 檢查異常goto quit;}}

AllocObject 函數(shù)創(chuàng)建的是一個未初始化的對象,后面在用這個對象之前,必須調用CallNonvirtualVoidMethod 調用對象的構造函數(shù)初始化該對象。而且在使用時一定要非常小心,確保在一個對象上面,構造函數(shù)最多被調用一次。有時,先創(chuàng)建一個初始化的對象,然后在合適的時間再調用構造函數(shù)的方式是很有用的。盡管如此,大部分情況下,應該使用 NewObject,盡量避免使用容易出錯的 AllocObject/CallNonvirtualVoidMethod 函數(shù)。

代碼講解 - 調用父類實例方法

如果一個方法被定義在父類中,在子類中被覆蓋,也可以調用父類中的這個實例方法。JNI 提供了一系列函數(shù)CallNonvirtualXXXMethod 來支持調用各種返回值類型的實例方法。調用一個定義在父類中的實例方法,須遵循下面的步驟。

使用 GetMethodID 函數(shù)從一個指向父類的 Class 引用當中獲取方法 ID。

cls_animal = (*env)->FindClass(env, "com/study/jnilearn/Animal"); if (cls_animal == NULL) {return; }//例1: 調用父類的run方法 mid_run = (*env)->GetMethodID(env, cls_animal, "run", "()V"); // 獲取父類Animal中run方法的id if (mid_run == NULL) {return; }

傳入子類對象、父類 Class 引用、父類方法 ID 和參數(shù),并調用 CallNonvirtualVoidMethod、 CallNonvirtualBooleanMethod、CallNonvirtualIntMethod 等一系列函數(shù)中的一個。其中CallNonvirtualVoidMethod 也可以被用來調用父類的構造函數(shù)。

// 注意:obj_cat是Cat的實例,cls_animal是Animal的Class引用,mid_run是Animal類中的方法ID (*env)->CallNonvirtualVoidMethod(env, obj_cat, cls_animal, mid_run);

其實在開發(fā)當中,這種調用父類實例方法的情況是很少遇到的,通常在 JAVA 中可以很簡單地做到:?super.func();但有些特殊需求也可能會用到,所以知道有這么回事還是很有必要的。

?

JNI 調用性能測試及優(yōu)化

在前面幾章我們學習到了,在 Java 中聲明一個 native 方法,然后生成本地接口的函數(shù)原型聲明,再用 C/C++ 實現(xiàn)這些函數(shù),并生成對應平臺的動態(tài)共享庫放到 Java 程序的類路徑下,最后在 Java 程序中調用聲明的 native 方法就間接的調用到了 C/C++ 編寫的函數(shù)了,在 C/C++ 中寫的程序可以避開 JVM 的內存開銷過大的限制、處理高性能的計算、調用系統(tǒng)服務等功能。同時也學習到了在本地代碼中通過 JNI 提供的接口,調用 Java 程序中的任意方法和對象的屬性。這是 JNI 提供的一些優(yōu)勢。但做過 Java 的童鞋應該都明白,Java 程序是運行在 JVM 上的,所以在 Java 中調用 C/C++ 或其它語言這種跨語言的接口時,或者說在 C/C++ 代碼中通過 JNI 接口訪問 Java 中對象的方法或屬性時,相比 Java 調用自已的方法,性能是非常低的!網上有朋友針對 Java 調用本地接口,Java 調 Java 方法做了一次詳細的測試,來充分說明在享受 JNI 給程序帶來優(yōu)勢的同時,也要接受其所帶來的性能開銷,請看下面一組測試數(shù)據(jù)。

Java 調用 JNI 空函數(shù)與 Java 調用 Java 空方法性能測試。

測試環(huán)境:JDK1.4.2_19、JDK1.5.0_04 和 JDK1.6.0_14,測試的重復次數(shù)都是一億次。測試結果的絕對數(shù)值意義不大,僅供參考。因為根據(jù) JVM 和機器性能的不同,測試所產生的數(shù)值也會不同,但不管什么機器和 JVM 應該都能反應同一個問題,Java 調用 native 接口,要比 Java 調用 Java 方法性能要低很多。

Java 調用 Java 空方法的性能:

JDK 版本Java 調 Java 耗時平均每秒調用次數(shù)
1.6329ms303951367次
1.5312ms320512820次
1.4312ms27233115次

Java 調用 JNI 空函數(shù)的性能:

JDK版本Java調Java耗時平均每秒調用次數(shù)
1.61531ms65316786次
1.51891ms52882072次
1.43672ms27233115次

從上述測試數(shù)據(jù)可以看出 JDK 版本越高,JNI 調用的性能也越好。在 JDK1.5 中,僅僅是空方法調用,JNI 的性能就要比 Java 內部調用慢將近 5 倍,而在 JDK1.4 下更是慢了十多倍。

JNI查找方法ID、字段ID、Class引用性能測試

當我們在本地代碼中要訪問 Java 對象的字段或調用它們的方法時,本機代碼必須調用 FindClass()、GetFieldID()、GetStaticFieldID、GetMethodID()和 GetStaticMethodID()。對于 GetFieldID()、GetStaticFieldID、GetMethodID() 和 GetStaticMethodID(),為特定類返回的 ID 不會在 JVM 進程的生存期內發(fā)生變化。但是,獲取字段或方法的調用有時會需要在 JVM 中完成大量工作,因為字段和方法可能是從超類中繼承而來的,這會讓 JVM 向上遍歷類層次結構來找到它們。由于 ID 對于特定類是相同的,因此只需要查找一次,然后便可重復使用。同樣,查找類對象的開銷也很大,因此也應該緩存它們。下面對調用 JNI 接口 FindClass 查找 Class、GetFieldID 獲取類的字段 ID 和 GetFieldValue 獲取字段的值的性能做的一個測試。緩存表示只調用一次,不緩存就是每次都調用相應的 JNI 接口:

java.version = 1.6.0_14

  • JNI 字段讀取 (緩存Class=false ,緩存字段ID=false) 耗時 : 79172 ms 平均每秒 : 1263072
  • JNI 字段讀取 (緩存Class=true ,緩存字段ID=false) 耗時 : 25015 ms 平均每秒 : 3997601
  • JNI 字段讀取 (緩存Class=false ,緩存字段ID=true) 耗時 : 50765 ms 平均每秒 : 1969861
  • JNI 字段讀取 (緩存Class=true ,緩存字段ID=true) 耗時 : 2125 ms 平均每秒 : 47058823

java.version = 1.5.0_04

  • JNI 字段讀取 (緩存Class=false ,緩存字段ID=false) 耗時 : 87109 ms 平均每秒 : 1147987
  • JNI 字段讀取 (緩存Class=true ,緩存字段ID=false) 耗時 : 32031 ms 平均每秒 : 3121975
  • JNI 字段讀取 (緩存Class=false ,緩存字段ID=true) 耗時 : 51657 ms 平均每秒 : 1935846
  • JNI 字段讀取 (緩存Class=true ,緩存字段ID=true) 耗時 : 2187 ms 平均每秒 : 45724737

java.version = 1.4.2_19

  • JNI 字段讀取 (緩存Class=false ,緩存字段ID=false) 耗時 : 97500 ms 平均每秒 : 1025641
  • JNI 字段讀取 (緩存Class=true ,緩存字段ID=false) 耗時 : 38110 ms 平均每秒 : 2623983
  • JNI 字段讀取 (緩存Class=false ,緩存字段ID=true) 耗時 : 55204 ms 平均每秒 : 1811462
  • JNI 字段讀取 (緩存Class=true ,緩存字段ID=true) 耗時 : 4187 ms 平均每秒 : 23883448

根據(jù)上面的測試數(shù)據(jù)得知,查找 class 和 ID (屬性和方法 ID)消耗的時間比較大。只是讀取字段值的時間基本上跟上面的 JNI 空方法是一個數(shù)量級。而如果每次都根據(jù)名稱查找 class 和 field 的話,性能要下降高達40倍。讀取一個字段值的性能在百萬級上,在交互頻繁的 JNI 應用中是不能忍受的。 消耗時間最多的就是查找class,因此在 native 里保存 class 和 member id 是很有必要的。class 和 member id 在一定范圍內是穩(wěn)定的,但在動態(tài)加載的 class loader 下,保存全局的 class 要么可能失效,要么可能造成無法卸載classloader,在諸如 OSGI 框架下的 JNI 應用還要特別注意這方面的問題。在讀取字段值和查找 FieldID 上,JDK1.4 和 1.5、1.6 的差距是非常明顯的。但在最耗時的查找 class 上,三個版本沒有明顯差距。

通過上面的測試可以明顯的看出,在調用 JNI 接口獲取方法 ID、字段 ID 和 Class 引用時,如果沒用使用緩存的話,性能低至 4 倍。所以在 JNI 開發(fā)中,合理的使用緩存技術能給程序提高極大的性能。緩存有兩種,分別為使用時緩存和類靜態(tài)初始化時緩存,區(qū)別主要在于緩存發(fā)生的時刻。

使用時緩存

字段 ID、方法 ID 和 Class 引用在函數(shù)當中使用的同時就緩存起來。下面看一個示例:

package com.study.jnilearn;public class AccessCache {private String str = "Hello";public native void accessField(); // 訪問str成員變量public native String newString(char[] chars, int len); // 根據(jù)字符數(shù)組和指定長度創(chuàng)建String對象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] = '國';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

實現(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) {// 第一次訪問時將字段存到內存數(shù)據(jù)區(qū),直到程序結束才會釋放,可以起到緩存的作用static jfieldID fid_str = NULL;jclass cls_AccessCache;jstring j_str;const char *c_str;cls_AccessCache = (*env)->GetObjectClass(env, obj); // 獲取該對象的Class引用if (cls_AccessCache == NULL) {return;}// 先判斷字段ID之前是否已經緩存過,如果已經緩存過則不進行查找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; // 內存不夠}printf("In C:\n str = \"%s\"\n", c_str);(*env)->ReleaseStringUTFChars(env, j_str, c_str); // 釋放從從JVM新分配字符串的內存空間// 修改字段的值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;// 注意:這里緩存局引用的做法是錯誤,這里做為一個反面教材提醒大家,下面會說到。if (cls_string == NULL) {cls_string = (*env)->FindClass(env, "java/lang/String");if (cls_string == NULL) {return NULL;}}// 緩存String的構造方法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)建一個字符數(shù)組elemArray = (*env)->NewCharArray(env, len);if (elemArray == NULL) {return NULL;}// 獲取數(shù)組的指針引用,注意:不能直接將jcharArray作為SetCharArrayRegion函數(shù)最后一個參數(shù)chars = (*env)->GetCharArrayElements(env, j_char_arr,NULL);if (chars == NULL) {return NULL;}// 將Java字符數(shù)組中的內容復制指定長度到新的字符數(shù)組中(*env)->SetCharArrayRegion(env, elemArray, 0, len, chars);// 調用String對象的構造方法,創(chuàng)建一個指定字符數(shù)組為內容的String對象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ù)中定義了一個靜態(tài)變量fid_str用于存儲字段的 ID,每次調用函數(shù)的時候

static jfieldID fid_str = NULL;

在代碼段

// 先判斷字段ID之前是否已經緩存過,如果已經緩存過則不進行查找if (fid_str == NULL) {fid_str = (*env)->GetFieldID(env,cls_AccessCache,"str","Ljava/lang/String;");// 再次判斷是否找到該類的str字段if (fid_str == NULL) {return;}}

判斷字段 ID 是否已經緩存,如果沒有先取出來存到fid_str中,下次再調用的時候該變量已經有值了,不用再去JVM中獲取,起到了緩存的作用。

在 Java_com_study_jnilearn_AccessCache_newString 函數(shù)中定義了兩個變量cls_string和cid_string,分別用于存儲 java.lang.String 類的 Class 引用和 String 的構造方法 ID。在使用前會先判斷是否已經緩存過,如果沒有則調用 JNI 的接口從 JVM 中獲取 String 的 Class 引用和構造方法 ID 存儲到靜態(tài)變量當中。下次再調用該函數(shù)時就可以直接使用,不需要再去找一次了,也達到了緩存的效果,大家第一反映都會這么認為。但是請注意:cls_string是一個局部引用,與方法和字段 ID 不一樣,局部引用在函數(shù)結束后會被 JVM 自動釋放掉,這時cls_string成為了一個野針對(指向的內存空間已被釋放,但變量的值仍然是被釋放后的內存地址,不為 NULL),當下次再調用 Java_com_xxxx_newString 這個函數(shù)的時候,會試圖訪問一個無效的局部引用,從而導致非法的內存訪問造成程序崩潰。所以在函數(shù)內用 static 緩存局部引用這種方式是錯誤的。下篇文章會介紹局部引用和全局引用,利用全局引用來防止這種問題,請關注。

類靜態(tài)初始化緩存

在調用一個類的方法或屬性之前,Java 虛擬機會先檢查該類是否已經加載到內存當中,如果沒有則會先加載,然后緊接著會調用該類的靜態(tài)初始化代碼塊,所以在靜態(tài)初始化該類的過程當中計算并緩存該類當中的字段 ID 和方法 ID 也是個不錯的選擇。下面看一個示例:

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 到內存當中之后,會調用該類的靜態(tài)初始化代碼塊,即 static 代碼塊,先調用System.loadLibrary 加載動態(tài)庫到 JVM 中,緊接著調用 native 方法 initIDs,會調用用到本地函數(shù)Java_com_study_jnilearn_AccessCache_initIDs,在該函數(shù)中獲取需要緩存的 ID,然后存入全局變量當中。下次需要用到這些 ID 的時候,直接使用全局變量當中的即可,調用 Java 的 callback 函數(shù)。

(*env)->CallVoidMethod(env, obj, MID_AccessCache_callback);

兩種緩存方式比較

如果在寫 JNI 接口時,不能控制方法和字段所在類的源碼的話,用使用時緩存比較合理。但比起類靜態(tài)初始化時緩存來說,用使用時緩存有一些缺點:

  • 使用前,每次都需要檢查是否已經緩存該 ID 或 Class 引用
  • 如果在用使用時緩存的 ID,要注意只要本地代碼依賴于這個 ID 的值,那么這個類就不會被 unload。另外一方面,如果緩存發(fā)生在靜態(tài)初始化時,當類被 unload 或 reload 時,ID 會被重新計算。因為,盡量在類靜態(tài)初始化時就緩存字段 ID、方法 ID 和類的 Class 引用。

?

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

這篇文章比較偏理論,詳細介紹了在編寫本地代碼時三種引用的使用場景和注意事項。可能看起來有點枯燥,但引用是在 JNI 中最容易出錯的一個點,如果使用不當,容易使程序造成內存溢出,程序崩潰等現(xiàn)象。《Android JNI局部引用表溢出》這篇文章是一個 JNI 引用使用不當造成引用表溢出,最終導致程序崩潰的例子。建議看完這篇文章之后,再去看。

做 Java 的朋友都知道,在編碼的過程當中,內存管理這一塊完全是透明的。new 一個類的實例時,只知道創(chuàng)建完這個類的實例之后,會返回這個實例的一個引用,然后就可以拿著這個引用訪問它的所有數(shù)據(jù)成員了(屬性、方法)。完全不用管 JVM 內部是怎么實現(xiàn)的,如何為新創(chuàng)建的對象來申請內存,也不用管對象使用完之后內存是怎么釋放的,只需知道有一個垃圾回器在幫忙管理這些事情就 OK 的了。有經驗的朋友也許知道啟動一個 Java 程序,如果沒有手動創(chuàng)建其它線程,默認會有兩個線程在跑,一個是 main 線程,另一個就是 GC 線程(負責將一些不再使用的對象回收)。如果你曾經是做 Java 的然后轉去做 C++,會感覺很不習慣,在 C++ 中 new 一個對象,使用完了還要做一次 delete 操作,malloc 一次同樣也要調用 free 來釋放相應的內存,否則你的程序就會有內存泄露了。而且在 C/C++ 中內存還分棧空間和堆空間,其中局部變量、函數(shù)形參變量、for 中定義的臨時變量所分配的內存空間都是存放在棧空間(而且還要注意大小的限制),用 new 和 malloc 申請的內存都存放在堆空間。但 C/C++ 里的內存管理還遠遠不止這些,這些只是最基礎的內存管理常識。做 Java 的人聽到這些肯定會偷樂了,咱寫 Java 的時候這些都不用管,全都交給 GC 就萬事無優(yōu)了。手動管理內存雖然麻煩,而且需要特別細心,一不小心就有可能造成內存泄露和野指針訪問等程序致命的問題,但凡事都有利弊,手動申請和釋放內存對程序的掌握比較靈活,不會受到平臺的限制。比如我們寫Android程序的時候,內存使用就受Dalivk虛擬機的限制,從最初版本的16~24M,到后來的 32M 到 64M,可能隨著以后移動設備物理內存的不大擴大,后面的 Android 版本內存限制可能也會隨著提高。但在 C/C++ 這層,就完全不受虛擬機的限制了。比如要在 Android 中要存儲一張超高清的圖片,剛好這張圖片的大小超過了 Dalivk 虛擬機對每個應用的內存大小限制,Java 此時就顯得無能為力了,但在C/C++ 看來就是小菜一碟了,malloc(1024102450)。C/C++ 程序員得意的說道,Java 不是說是一門純面象對象的語言嗎,所以除了基本數(shù)據(jù)類型外,其它任何類型所創(chuàng)建的對象,JVM 所申請的內存都存在堆空間。上面提高到了 GC,是負責回收不再使用的對象,它的全稱是 Garbage Collection,也就是所謂的垃圾回收。JVM 會在適當?shù)臅r機觸發(fā) GC 操作,一旦進行 GC 操作,就會將一些不再使用的對象進行回收。那么哪些對象會被認為是不再使用,并且可以被回收的呢?我們來看下面二張圖。(注:圖摘自博主郭霖的《Android 最佳性能實踐(二)——分析內存的使用情況》)

上圖當中,每個藍色的圓圈就代表一個內存當中的對象,而圓圈之間的箭頭就是它們的引用關系。這些對象有些是處于活動狀態(tài)的,而有些就已經不再被使用了。那么 GC 操作會從一個叫作 Roots 的對象開始檢查,所有它可以訪問到的對象就說明還在使用當中,應該進行保留,而其它的對象就表示已經不再被使用了,如下圖所示:

可以看到,目前所有黃色的對象都處于活動狀態(tài),仍然會被系統(tǒng)繼續(xù)保留,而藍色的對象就會在 GC 操作當中被系統(tǒng)回收掉了,這就是 JVM 執(zhí)行一次 GC 的簡單流程。

上面說的廢話好像有點多哈,下面進入正題。通過上面的討論,大家都知道,如果一個 Java 對象沒有被其它成員變量或靜態(tài)變量所引用的話,就隨時有可能會被 GC 回收掉。所以我們在編寫本地代碼時,要注意從 JVM 中獲取到的引用在使用時被 GC 回收的可能性。由于本地代碼不能直接通過引用操作 JVM 內部的數(shù)據(jù)結構,要進行這些操作必須調用相應的 JNI 接口來間接操作所引用的數(shù)據(jù)結構。JNI 提供了和 Java 相對應的引用類型,供本地代碼配合 JNI 接口間接操作 JVM 內部的數(shù)據(jù)內容使用。如:jobject、jstring、jclass、jarray、jintArray 等。因為我們只通過 JNI 接口操作 JNI 提供的引用類型數(shù)據(jù)結構,而且每個 JVM 都實現(xiàn)了 JNI 規(guī)范相應的接口,所以我們不必擔心特定 JVM 中對象的存儲方式和內部數(shù)據(jù)結構等信息,我們只需要學習 JNI 中三種不同的引用即可。

由于 Java 程序運行在虛擬機中的這個特點,在 Java 中創(chuàng)建的對象、定義的變量和方法,內部對象的數(shù)據(jù)結構是怎么定義的,只有 JVM 自己知道。如果我們在 C/C++ 中想要訪問 Java 中對象的屬性和方法時,是不能夠直接操作 JVM 內部 Java 對象的數(shù)據(jù)結構的。想要在 C/C++ 中正確的訪問 Java 的數(shù)據(jù)結構,JVM 就必須有一套規(guī)則來約束 C/C++ 與 Java 互相訪問的機制,所以才有了 JNI 規(guī)范,JNI 規(guī)范定義了一系列接口,任何實現(xiàn)了這套 JNI 接口的 Java 虛擬機,C/C++ 就可以通過調用這一系列接口來間接的訪問 Java 中的數(shù)據(jù)結構。比如前面文章中學習到的常用 JNI 接口有:GetStringUTFChars(從 Java 虛擬機中獲取一個字符串)、ReleaseStringUTFChars(釋放從 JVM 中獲取字符串所分配的內存空間)、NewStringUTF、GetArrayLength、GetFieldID、GetMethodID、FindClass 等。

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

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

局部引用

通過 NewLocalRef 和各種 JNI 接口創(chuàng)建(FindClass、NewObject、GetObjectClass和NewCharArray等)。會阻止 GC 回收所引用的對象,不在本地函數(shù)中跨函數(shù)使用,不能跨線前使用。函數(shù)返回后局部引用所引用的對象會被JVM 自動釋放,或調用 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); // 通過NewLocalRef函數(shù)創(chuàng)建 ...

全局引用

調用 NewGlobalRef 基于局部引用創(chuàng)建,會阻 GC 回收所引用的對象。可以跨方法、跨線程使用。JVM 不會自動釋放,必須調用 DeleteGlobalRef 手動釋放。(*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); }

弱全局引用

調用 NewWeakGlobalRef 基于局部引用或全局引用創(chuàng)建,不會阻止 GC 回收所引用的對象,可以跨方法、跨線程使用。引用不會自動釋放,在 JVM 認為應該回收它的時候(比如內存緊張的時候)進行回收而被釋放。或調用DeleteWeakGlobalRef 手動釋放。(*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)建并使用。會阻止 GC 回收所引用的對象。比如,調用 NewObject 接口創(chuàng)建一個新的對象實例并返回一個對這個對象的局部引用。局部引用只有在創(chuàng)建它的本地方法返回前有效,本地方法返回到 Java 層之后,如果 Java 層沒有對返回的局部引用使用的話,局部引用就會被 JVM 自動釋放。你可能會為了提高程序的性能,在函數(shù)中將局部引用存儲在靜態(tài)變量中緩存起來,供下次調用時使用。這種方式是錯誤的,因為函數(shù)返回后局部引很可能馬上就會被釋放掉,靜態(tài)變量中存儲的就是一個被釋放后的內存地址,成了一個野針對,下次再使用的時候就會造成非法地址的訪問,使程序崩潰。請看下面一個例子,錯誤的緩存了 String 的 Class 引用。

/*錯誤的局部引用*/ 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;// 注意:錯誤的引用緩存if (cls_string == NULL) {cls_string = (*env)->FindClass(env, "java/lang/String");if (cls_string == NULL) {return NULL;}}// 緩存String的構造方法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; }

上面代碼中,我們省略了和我們討論無關的代碼。因為 FindClass 返回一個對 java.lang.String 對象的局部引用,上面代碼中緩存 cls_string 做法是錯誤的。假設一個本地方法 C.f 調用了 newString。

JNIEXPORT jstring JNICALLJava_C_f(JNIEnv *env, jobject this){char *c_str = ...;...return newString(c_str); }

Java_com_study_jnilearn_AccessCache_newString 下面簡稱 newString。

C.f 方法返回后,JVM 會釋放在這個方法執(zhí)行期間創(chuàng)建的所有局部引用,也包含對 String 的 Class 引用cls_string。當再次調用 newString 時,newString 所指向引用的內存空間已經被釋放,成為了一個野指針,再訪問這個指針的引用時,會導致因非法的內存訪問造成程序崩潰。

... ... = C.f(); // 第一次調是OK的 ... = C.f(); // 第二次調用時,訪問的是一個無效的引用. ...

釋放局部引用

釋放一個局部引用有兩種方式,一個是本地方法執(zhí)行完畢后 JVM 自動釋放,另外一個是自己調用 DeleteLocalRef 手動釋放。既然 JVM 會在函數(shù)返回后會自動釋放所有局部引用,為什么還需要手動釋放呢?大部分情況下,我們在實現(xiàn)一個本地方法時不必擔心局部引用的釋放問題,函數(shù)被調用完成后,JVM 會自動釋放函數(shù)中創(chuàng)建的所有局部引用。盡管如此,以下幾種情況下,為了避免內存溢出,我們應該手動釋放局部引用。

JNI 會將創(chuàng)建的局部引用都存儲在一個局部引用表中,如果這個表超過了最大容量限制,就會造成局部引用表溢出,使程序崩潰。經測試,Android 上的 JNI 局部引用表最大數(shù)量是 512 個。當我們在實現(xiàn)一個本地方法時,可能需要創(chuàng)建大量的局部引用,如果沒有及時釋放,就有可能導致 JNI 局部引用表的溢出,所以,在不需要局部引用時就立即調用 DeleteLocalRef 手動刪除。比如,在下面的代碼中,本地代碼遍歷一個特別大的字符串數(shù)組,每遍歷一個元素,都會創(chuàng)建一個局部引用,當對使用完這個元素的局部引用時,就應該馬上手動釋放它。

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

在編寫 JNI 工具函數(shù)時,工具函數(shù)在程序當中是公用的,被誰調用你是不知道的。上面 newString 這個函數(shù)演示了怎么樣在工具函數(shù)中使用完局部引用后,調用 DeleteLocalRef 刪除。不這樣做的話,每次調用 newString 之后,都會遺留兩個引用占用空間(elemArray和cls_string,cls_string 不用 static 緩存的情況下)。

如果你的本地函數(shù)不會返回。比如一個接收消息的函數(shù),里面有一個死循環(huán),用于等待別人發(fā)送消息過來while(true) { if (有新的消息) { 處理之。。。。} else { 等待新的消息。。。}}。如果在消息循環(huán)當中創(chuàng)建的引用你不顯示刪除,很快將會造成 JVM 局部引用表溢出。

局部引用會阻止所引用的對象被 GC 回收。比如你寫的一個本地函數(shù)中剛開始需要訪問一個大對象,因此一開始就創(chuàng)建了一個對這個對象的引用,但在函數(shù)返回前會有一個大量的非常復雜的計算過程,而在這個計算過程當中是不需要前面創(chuàng)建的那個大對象的引用的。但是,在計算的過程當中,如果這個大對象的引用還沒有被釋放的話,會阻止 GC 回收這個對象,內存一直占用者,造成資源的浪費。所以這種情況下,在進行復雜計算之前就應該把引用給釋放了,以免不必要的資源浪費。

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

管理局部引用

JNI 提供了一系列函數(shù)來管理局部引用的生命周期。這些函數(shù)包括:EnsureLocalCapacity、NewLocalRef、PushLocalFrame、PopLocalFrame、DeleteLocalRef。JNI 規(guī)范指出,任何實現(xiàn) JNI 規(guī)范的 JVM,必須確保每個本地函數(shù)至少可以創(chuàng)建 16 個局部引用(可以理解為虛擬機默認支持創(chuàng)建 16 個局部引用)。實際經驗表明,這個數(shù)量已經滿足大多數(shù)不需要和 JVM 中內部對象有太多交互的本地方函數(shù)。如果需要創(chuàng)建更多的引用,可以通過調用 EnsureLocalCapacity 函數(shù),確保在當前線程中創(chuàng)建指定數(shù)量的局部引用,如果創(chuàng)建成功則返回 0,否則創(chuàng)建失敗,并拋出 OutOfMemoryError 異常。EnsureLocalCapacity 這個函數(shù)是 1.2 以上版本才提供的,為了向下兼容,在編譯的時候,如果申請創(chuàng)建的局部引用超過了本地引用的最大容量,在運行時 JVM 會調用 FatalError 函數(shù)使程序強制退出。在開發(fā)過程當中,可以為 JVM 添加-verbose:jni參數(shù),在編譯的時如果發(fā)現(xiàn)本地代碼在試圖申請過多的引用時,會打印警告信息提示我們要注意。在下面的代碼中,遍歷數(shù)組時會獲取每個元素的引用,使用完了之后不手動刪除,不考慮內存因素的情況下,它可以為這種創(chuàng)建大量的局部引用提供足夠的空間。由于沒有及時刪除局部引用,因此在函數(shù)執(zhí)行期間,會消耗更多的內存。

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

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

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

PushLocalFrame 為當前函數(shù)中需要用到的局部引用創(chuàng)建了一個引用堆棧,(如果之前調用 PushLocalFrame 已經創(chuàng)建了 Frame,在當前的本地引用棧中仍然是有效的)每遍歷一次調用(*env)->GetObjectArrayElement(env, arr, i);返回一個局部引用時,JVM 會自動將該引用壓入當前局部引用棧中。而 PopLocalFrame 負責銷毀棧中所有的引用。這樣一來,Push/PopLocalFrame 函數(shù)對提供了對局部引用生命周期更方便的管理,而不需要時刻關注獲取一個引用后,再調用 DeleteLocalRef 來釋放引用。在上面的例子中,如果在處理 jstr 的過程當中又創(chuàng)建了局部引用,則 PopLocalFrame 執(zhí)行時,這些局部引用將全都會被銷毀。在調用 PopLocalFrame 銷毀當前 frame 中的所有引用前,如果第二個參數(shù) result 不為空,會由 result 生成一個新的局部引用,再把這個新生成的局部引用存儲在上一個 frame 中。請看下面的示例。

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

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

全局引用

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

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引用緩存到全局引用當中cls_string = (*env)->NewGlobalRef(env, local_cls_string);// 刪除局部引用(*env)->DeleteLocalRef(env, local_cls_string);// 再次驗證全局引用是否創(chuàng)建成功if (cls_string == NULL) {return NULL;}}// ....return jstr; }

弱全局引用

弱全局引用使用 NewGlobalWeakRef 創(chuàng)建,使用 DeleteGlobalWeakRef 釋放。下面簡稱弱引用。與全局引用類似,弱引用可以跨方法、線程使用。但與全局引用很重要不同的一點是,弱引用不會阻止 GC 回收它引用的對象。在newString 這個函數(shù)中,我們也可以使用弱引用來存儲 String 的 Class 引用,因為 java.lang.String 這個類是系統(tǒng)類,永遠不會被 GC 回收。當本地代碼中緩存的引用不一定要阻止 GC 回收它所指向的對象時,弱引用就是一個最好的選擇。假設,一個本地方法mypkg.MyCls.f需要緩存一個指向類mypkg.MyCls2的引用,如果在弱引用中緩存的話,仍然允許mypkg.MyCls2這個類被 unload,因為弱引用不會阻止 GC 回收所引用的對象。請看下面的代碼段。

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; /* 沒有找到mypkg/MyCls2這個類 */}myCls2 = NewWeakGlobalRef(env, myCls2Local);if (myCls2 == NULL){return; /* 內存溢出 */}}... /* 使用myCls2的引用 */ }

我們假設 MyCls 和 MyCls2 有相同的生命周期(例如,他們可能被相同的類加載器加載),因為弱引用的存在,我們不必擔心 MyCls 和它所在的本地代碼在被使用時,MyCls2 這個類出現(xiàn)先被 unload,后來又會 preload 的情況。當然,如果真的發(fā)生這種情況時(MyCls 和 MyCls2 此時的生命周期不同),我們在使用弱引用時,必須先檢查緩存過的弱引用是指向活動的類對象,還是指向一個已經被 GC 給 unload 的類對象。下面馬上告訴你怎樣檢查弱引用是否活動,即引用的比較。

引用比較

給定兩個引用(不管是全局、局部還是弱全局引用),我們只需要調用 IsSameObject 來判斷它們兩個是否指向相同的對象。例如:(*env)->IsSameObject(env, obj1, obj2),如果 obj1 和 obj2 指向相同的對象,則返回 JNI_TRUE(或者 1),否則返回 JNI_FALSE(或者 0)。有一個特殊的引用需要注意:NULL,JNI 中的 NULL 引用指向 JVM 中的 null 對象。如果 obj 是一個局部或全局引用,使用(*env)->IsSameObject(env, obj, NULL)?或者obj == NULL?來判斷 obj 是否指向一個 null 對象即可。但需要注意的是,IsSameObject 用于弱全局引用與 NULL 比較時,返回值的意義是不同于局部引用和全局引用的。

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

在上面的 IsSameObject 調用中,如果 g_obj_ref 指向的引用已經被回收,會返回 JNI_TRUE,如果 wobj 仍然指向一個活動對象,會返回 JNI_FALSE。

釋放全局引用

每一個 JNI 引用被建立時,除了它所指向的 JVM 中對象的引用需要占用一定的內存空間外,引用本身也會消耗掉一個數(shù)量的內存空間。作為一個優(yōu)秀的程序員,我們應該對程序在一個給定的時間段內使用的引用數(shù)量要十分小心。短時間內創(chuàng)建大量而沒有被立即回收的引用很可能就會導致內存溢出。 ??? 當我們的本地代碼不再需要一個全局引用時,應該馬上調用 DeleteGlobalRef 來釋放它。如果不手動調用這個函數(shù),即使這個對象已經沒用了,JVM 也不會回收這個全局引用所指向的對象。 ???? 同樣,當我們的本地代碼不再需要一個弱全局引用時,也應該調用 DeleteWeakGlobalRef 來釋放它,如果不手動調用這個函數(shù)來釋放所指向的對象,JVM 仍會回收弱引用所指向的對象,但弱引用本身在引用表中所占的內存永遠也不會被回收。

管理引用的規(guī)則

前面對三種引用已做了一個全面的介紹,下面來總結一下引用的管理規(guī)則和使用時的一些注意事項,使用好引用的目的就是為了減少內存使用和對象被引用保持而不能釋放,造成內存浪費。所以在開發(fā)當中要特別小心!

通常情況下,有兩種本地代碼使用引用時要注意:

  • 直接實現(xiàn)Java層聲明的native函數(shù)的本地代碼 當編寫這類本地代碼時,要當心不要造成全局引用和弱引用的累加,因為本地方法執(zhí)行完畢后,這兩種引用不會被自動釋放。
  • 被用在任何環(huán)境下的工具函數(shù)。例如:方法調用、屬性訪問和異常處理的工具函數(shù)等。

編寫工具函數(shù)的本地代碼時,要當心不要在函數(shù)的調用軌跡上遺漏任何的局部引用,因為工具函數(shù)被調用的場合和次數(shù)是不確定的,一量被大量調用,就很有可能造成內存溢出。所以在編寫工具函數(shù)時,請遵守下面的規(guī)則:

  • 一個返回值為基本類型的工具函數(shù)被調用時,它決不能造成局部、全局、弱全局引用被回收的累加。
  • 當一個返回值為引用類型的工具函數(shù)被調用時,它除了返回的引用以外,它決不能造成其它局部、全局、弱引用的累加。

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

while (JNI_TRUE) {jstring infoString = GetInfoString(info);... /* 處理infoString */??? /* 使用完成之后,調用DeleteLocalRef、DeleteGlobalRef、DeleteWeakGlobalRef哪一個函數(shù)來釋放這個引用呢?*/ }

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

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString {static jstring result;/* 使用wstrncmp函數(shù)比較兩個Unicode字符串 */if (wstrncmp("CommonString", chars, len) == 0){/* 將"CommonString"這個字符串緩存到全局引用中 */static jstring cachedString = NULL;if (cachedString == NULL){/* 先創(chuàng)建"CommonString"這個字符串 */jstring cachedStringLocal = ...;/* 然后將這個字符串緩存到全局引用中 */cachedString = (*env)->NewGlobalRef(env, cachedStringLocal);}// 基于全局引用創(chuàng)建一個局引用返回,也同樣會阻止GC回收所引用的這個對象,因為它們指向的是同一個對象return (*env)->NewLocalRef(env, cachedString); }... return result; }

在管理局部引用的生命周期中,Push/PopLocalFrame 是非常方便且安全的。我們可以在本地函數(shù)的入口處調用PushLocalFrame,然后在出口處調用 PopLocalFrame,這樣的話,在函數(shù)內任何位置創(chuàng)建的局部引用都會被釋放。而且,這兩個函數(shù)是非常高效的,強烈建議使用它們。需要注意的是,如果在函數(shù)的入口處調用了PushLocalFrame,記住要在函數(shù)所有出口(有 return 語句出現(xiàn)的地方)都要調用 PopLocalFrame。在下面的代碼中,對 PushLocalFrame 的調用只有一次,但調用 PopLocalFrame 確有多次,當然你也可以使用 goto 語句來統(tǒng)一處理。

jobject f(JNIEnv *env, ...) {jobject result;if ((*env)->PushLocalFrame(env, 10) < 0){/* 調用PushLocalFrame獲取10個局部引用失敗,不需要調用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 的第二個參數(shù)的用法,局部引用 result 一開始在 PushLocalFrame 創(chuàng)建在當前 frame 里面,而把 result 傳入 PopLocalFrame 中時,PopLocalFrame 在彈出當前的 frame 前,會由 result 生成一個新的局部引用,再將這個新生成的局部引用存儲在上一個 frame 當中。

?

編譯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

轉載于:https://www.cnblogs.com/EMH899/p/10800644.html

總結

以上是生活随笔為你收集整理的jni2的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。