在前面幾章我們學習到了,在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引用在函數當中使用的同時就緩存起來。下面看一個示例:
~~~
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**
~~~
/* 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**
~~~
// 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;
// 注意:這里緩存局引用的做法是錯誤,這里做為一個反面教材提醒大家,下面會說到。
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: %d\n", 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存儲到靜態變量當中。下次再調用該函數時就可以直接使用,不需要再去找一次了,也達到了緩存的效果,大家第一反映都會這么認為。但是請注意:cls_string是一個局部引用,與方法和字段ID不一樣,局部引用在函數結束后會被VM自動釋放掉,這時cls_string成為了一個野針對(指向的內存空間已被釋放,但變量的值仍然是被釋放后的內存地址,不為NULL),當下次再調用Java_com_xxxx_newString這個函數的時候,會試圖訪問一個無效的局部引用,從而導致非法的內存訪問造成程序崩潰。所以在函數內用static緩存局部引用這種方式是錯誤的。下篇文章會介紹局部引用和全局引用,利用全局引用來防止這種問題,請關注。
## 類靜態初始化緩存
在調用一個類的方法或屬性之前,Java虛擬機會先檢查該類是否已經加載到內存當中,如果沒有則會先加載,然后緊接著會調用該類的靜態初始化代碼塊,所以在靜態初始化該類的過程當中計算并緩存該類當中的字段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到內存當中之后,會調用該類的靜態初始化代碼塊,即static代碼塊,先調用System.loadLibrary加載動態庫到JVM中,緊接著調用native方法initIDs,會調用用到本地函數Java_com_study_jnilearn_AccessCache_initIDs,在該函數中獲取需要緩存的ID,然后存入全局變量當中。下次需要用到這些ID的時候,直接使用全局變量當中的即可,如18行當中調用Java的callback函數。
~~~
(*env)->CallVoidMethod(env, obj, MID_AccessCache_callback);
~~~
## 兩種緩存方式比較
如果在寫JNI接口時,不能控制方法和字段所在類的源碼的話,用使用時緩存比較合理。但比起類靜態初始化時緩存來說,用使用時緩存有一些缺點:
1. 使用前,每次都需要檢查是否已經緩存該ID或Class引用
2. 如果在用使用時緩存的ID,要注意只要本地代碼依賴于這個ID的值,那么這個類就不會被unload。另外一方面,如果緩存發生在靜態初始化時,當類被unload或reload時,ID會被重新計算。因為,盡量在類靜態初始化時就緩存字段ID、方法ID和類的Class引用。
- 前言
- JNI/NDK開發指南(開山篇)
- JNI/NDK開發指南(一)—— JNI開發流程及HelloWorld
- JNI/NDK開發指南(二)——JVM查找java native方法的規則
- JNI/NDK開發指南(三)——JNI數據類型及與Java數據類型的映射關系
- JNI/NDK開發指南(四)——字符串處理
- Android NDK開發Crash錯誤定位
- JNI/NDK開發指南(五)——訪問數組(基本類型數組與對象數組)
- JNI/NDK開發指南(六)——C/C++訪問Java實例方法和靜態方法
- JNI/NDK開發指南(七)——C/C++訪問Java實例變量和靜態變量
- JNI/NDK開發指南(八)——調用構造方法和父類實例方法
- JNI/NDK開發指南(九)——JNI調用性能測試及優化
- JNI/NDK開發指南(十)——JNI局部引用、全局引用和弱全局引用
- Android JNI局部引用表溢出:local reference table overflow (max=512)
- JNI/NDK開發指南(十一)——JNI異常處理