在前面幾章我們學習到了,在Java中宣告一個native方法,然後生成本地介面的函式原型宣告,再用C/C++實現這些函式,並生成對應平臺的動態共享庫放到Java程式的類路徑下,最後在Java程式中呼叫宣告的native方法就間接的呼叫到了C/C++編寫的函式了,在C/C++中寫的程式可以避開JVM的記憶體開銷過大的限制、處理高效能的計算、呼叫系統服務等功能。同時也學習到了在原生程式碼中通過JNI提供的介面,呼叫Java程式中的任意方法和物件的屬性。這是JNI提供的一些優勢。但做過Java的童鞋應該都明白,Java程式是執行在JVM上的,所以在Java中呼叫C/C++或其它語言這種跨語言的介面時,或者說在C/C++程式碼中通過JNI介面訪問Java中物件的方法或屬性時,相比Java呼叫自已的方法,效能是非常低的!!!網上有朋友針對Java呼叫本地介面,Java調Java方法做了一次詳細的測試,來充分說明在享受JNI給程式帶來優勢的同時,也要接受其所帶來的效能開銷,下面請看一組測試資料:
Java呼叫JNI空函式與Java呼叫Java空方法效能測試
測試環境:JDK1.4.2_19、JDK1.5.0_04和JDK1.6.0_14,測試的重複次數都是一億次。測試結果的絕對數值意義不大,僅供參考。因為根據JVM和機器效能的不同,測試所產生的數值也會不同,但不管什麼機器和JVM應該都能反應同一個問題,Java呼叫native介面,要比Java呼叫Java方法效能要低很多。
Java呼叫Java空方法的效能:
JDK版本 | Java調Java耗時 | 平均每秒呼叫次數 |
---|---|---|
1.6 | 329ms | 303951367次 |
1.5 | 312ms | 320512820次 |
1.4 | 312ms | 27233115次 |
Java呼叫JNI空函式的效能:
JDK版本 | Java調JNI耗時 | 平均每秒呼叫次數 |
---|---|---|
1.6 | 1531ms | 65316786次 |
1.5 | 1891ms | 52882072次 |
1.4 | 3672ms | 27233115次 |
從上述測試資料可以看出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 程式的生存期內發生變化。但是,獲取欄位或方法的呼叫有時會需要在 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
根據上面的測試資料得知,查詢class和ID(屬性和方法ID)消耗的時間比較大。只是讀取欄位值的時間基本上跟上面的JNI空方法是一個數量級。而如果每次都根據名稱查詢class和field的話,效能要下降高達40倍。讀取一個欄位值的效能在百萬級上,在互動頻繁的JNI應用中是不能忍受的。 消耗時間最多的就是查詢class,因此在native裡儲存class和member id是很有必要的。class和member id在一定範圍內是穩定的,但在動態載入的class loader下,儲存全域性的class要麼可能失效,要麼可能造成無法解除安裝classloader,在諸如OSGI框架下的JNI應用還要特別注意這方面的問題。在讀取欄位值和查詢FieldID上,JDK1.4和1.5、1.6的差距是非常明顯的。但在最耗時的查詢class上,三個版本沒有明顯差距。
通過上面的測試可以明顯的看出,在呼叫JNI介面獲取方法ID、欄位ID和Class引用時,如果沒用使用快取的話,效能低至4倍。所以在JNI開發中,合理的使用快取技術能給程式提高極大的效能。快取有兩種,分別為使用時快取和類靜態初始化時快取,區別主要在於快取發生的時刻。
使用時快取
欄位ID、方法ID和Class引用在函式當中使用的同時就快取起來。下面看一個示例:
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 |
package com.study.jnilearn; public class AccessCache { private String str = "Hello"; public native void accessField(); // 訪問str成員變數 public native String newString(char[] chars, int len); // 根據字元陣列和指定長度建立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
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 |
/* 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 |
實現標頭檔案中的函式:AccessCache.c
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
// AccessCache.c #include "com_study_jnilearn_AccessCache.h" JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_accessField (JNIEnv *env, jobject obj) { // 第一次訪問時將欄位存到記憶體資料區,直到程式結束才會釋放,可以起到快取的作用 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; // 快取String的class引用 if (cls_string == NULL) { cls_string = (*env)->FindClass(env, "java/lang/String"); if (cls_string == NULL) { return NULL; } } // 快取String的構造方法ID if (cid_string == NULL) { cid_string = (*env)->GetMethodID(env, cls_string, "<init>", "([C)V"); if (cid_string == NULL) { return NULL; } } printf("In C array Len: %dn", len); // 建立一個字元陣列 elemArray = (*env)->NewCharArray(env, len); if (elemArray == NULL) { return NULL; } // 獲取陣列的指標引用,注意:不能直接將jcharArray作為SetCharArrayRegion函式最後一個引數 chars = (*env)->GetCharArrayElements(env, j_char_arr,NULL); if (chars == NULL) { return NULL; } // 將Java字元陣列中的內容複製指定長度到新的字元陣列中 (*env)->SetCharArrayRegion(env, elemArray, 0, len, chars); // 呼叫String物件的構造方法,建立一個指定字元陣列為內容的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函式中的第8行定義了一個靜態變數fid_str用於儲存欄位的ID,每次呼叫函式的時候,在第18行先判斷欄位ID是否已經快取,如果沒有先取出來存到fid_str中,下次再呼叫的時候該變數已經有值了,不用再去JVM中獲取,起到了快取的作用。
例2、在Java_com_study_jnilearn_AccessCache_newString函式中的53和54行定義了兩個變數cls_string和cid_string,分別用於儲存java.lang.String類的Class引用和String的構造方法ID。在56行和64行處,使用前會先判斷是否已經快取過,如果沒有則呼叫JNI的介面從JVM中獲取String的Class引用和構造方法ID儲存到靜態變數當中。下次再呼叫該函式時就可以直接使用,不需要再去找一次了,也達到了快取的效果。
類靜態初始化快取
在呼叫一個類的方法或屬性之前,Java虛擬機器會先檢查該類是否已經載入到記憶體當中,如果沒有則會先載入,然後緊接著會呼叫該類的靜態初始化程式碼塊,所以在靜態初始化該類的過程當中計算並快取該類當中的欄位ID和方法ID也是個不錯的選擇。下面看一個示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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(); } } |
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 |
/* 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 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 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到記憶體當中之後,會呼叫該類的靜態初始化程式碼塊,即static程式碼塊,先呼叫System.loadLibrary載入動態庫到JVM中,緊接著呼叫native方法initIDs,會呼叫用到本地函式Java_com_study_jnilearn_AccessCache_initIDs,在該函式中獲取需要快取的ID,然後存入全域性變數當中。下次需要用到這些ID的時候,直接使用全域性變數當中的即可,如18行當中呼叫Java的callback函式。
1 |
(*env)->CallVoidMethod(env, obj, MID_AccessCache_callback); |
兩種快取方式比較
如果在寫JNI介面時,不能控制方法和欄位所在類的原始碼的話,用使用時快取比較合理。但比起類靜態初始化時快取來說,用使用時快取有一些缺點:
1. 使用前,每次都需要檢查是否已經快取該ID或Class引用
2. 如果在用使用時快取的ID,要注意只要原生程式碼依賴於這個ID的值,那麼這個類就不會被unload。另外一方面,如果快取發生在靜態初始化時,當類被unload或reload時,ID會被重新計算。因為,儘量在類靜態初始化時就快取欄位ID、方法ID和類的Class引用。