下面介紹2個與JNI機制相關的類型JNIEnv和JavaVM。
1、JNIEnv
JNIEnv一般是是由虛擬機傳入,而且與線程相關的變量,也就說線程A不能使用線程B的JNIEnv。而作為一個結構體,它里面定義了JNI系統操作函數
。在之前介紹的實例中,可以看到C的Java_TestJNI_set()或Java_TestJNI_get()函數的實現中,第1個參數的類型為JNIEnv*。JNIEnv的定義如下:
來源:openjdk/hotspot/src/share/vm/prims/jni.h struct JNIEnv_; #ifdef __cplusplus typedef JNIEnv_ JNIEnv; #else typedef const struct JNINativeInterface_ *JNIEnv; #endif
JNIEnv在C語言環境和C++語言環境中的實現是不一樣。在C中定義為JNINativeInterface_,在C++中定義為JNIEnv_。
JNIEnv_結構體的定義如下:
struct JNIEnv_ { const struct JNINativeInterface_ *functions; #ifdef __cplusplus jint GetVersion() { return functions->GetVersion(this); } jclass DefineClass(const char *name, jobject loader, const jbyte *buf,jsize len) { return functions->DefineClass(this, name, loader, buf, len); } jclass FindClass(const char *name) { return functions->FindClass(this, name); } ... #endif }
在JNIEnv_中定義了一個functions變量,這個變量是指向JNINativeInterface_的指針。所以如果我們在寫native函數時,當接收到類型為JNIEnv*的變量env時,可以使用如下方式調用JNIEnv中的函數(准確說是通過函數指針來調用函數,因為JNIEnv
的數據結構聚合了所有 JNI 函數的函數指針),我們可在C++中通過如下方式調用:
env->FindClass("java/lang/String") // C++中的寫法
而在C中可以通過如下方式調用:
(*env)->FindClass(env, "java/lang/String") // C中的寫法
而由於變量functions是定義在結構體JNIEnv_的第1個變量,所以我們通過*env就能獲取到functions變量的值,然后通過JNINativeInterface中的函數指針來調用對應的函數。
JNINativeInterface_結構體的定義如下:
struct JNINativeInterface_ { void *reserved0; void *reserved1; void *reserved2; void *reserved3; jint (JNICALL *GetVersion) (JNIEnv *env); jclass (JNICALL *DefineClass) (JNIEnv *env, const char *name, jobject loader, const jbyte *buf,jsize len); jclass (JNICALL *FindClass) (JNIEnv *env, const char *name); // ... }
下面就來介紹一下JNIEnv_結構體中保存的函數指針對應的函數,如下:
(1)jclass類型表示Java中的Class類。JNIEnv_結構體中有如下幾個簡單的函數(其實是通過函數指針調用對應的函數,我們直接表達為函數,后面也采用類似的表達方式)可以取得jclass:
- jclass FindClass(const char* clsName):通過類的名稱(類的全名,這時候包名不是用.號,而是用/來區分的)來獲取jclass,如:jclass str = env->FindClass("java/lang/String");獲取Java中的String對象的class實例;
- jclass GetObjectClass(jobject obj):通過對象實例來獲取jclass,相當於Java中的getClass()方法;
- jclass GetSuperClass(jclass obj):通過jclass可以獲取其父類的jclass實例。
(2)引用相關的API
- jobject NewGlobalRef(JNIEnv *env, jobject obj):創建一個指向obj的全局引用,obj可以是本地或者全局引用,全局引用只能通過顯示調用DeleteGlobalRef()釋放;
- void DeleteGlobalRef(JNIEnv *env, jobject globalRef):刪除一個全局引用;
- jobject NewLocalRef(JNIEnv *env, jobject ref):創建一個指向實例ref的本地引用;
- void DeleteLocalRef(JNIEnv *env, jobject localRef):刪除一個本地引用;
- jint EnsureLocalCapacity(JNIEnv *env, jint capacity):評估當前線程是否能夠創建指定數量的本地引用,如果可以返回0,否則返回負數並拋出OutOfMemoryError異常。在執行本地方法前JVM會自動評估當前線程能否創建至少16個本地引用。JVM允許創建超過評估數量的本地引用,如果創建過多導致JVM內存不足JVM會拋出一個FatalError;
- jint PushLocalFrame(JNIEnv *env, jint capacity):創建一個新的支持創建給定數量的本地引用的Frame,如果可以返回0,否則返回負數並拋出OutOfMemoryError異常。注意在之前的Frame中創建的本地引用在新的Frame中依然有效
- jobject PopLocalFrame(JNIEnv *env, jobject result):彈出掉當前的本地引用Frame,然后釋放其中的所有本地引用,如果result不為NULL,則返回該對象在前一個即當前Frame之前被push的Frame中的本地引用。PushLocalFrame和PopLocalFrame兩個都是配合使用,常見於方法執行過程中產生的本地引用需要盡快釋放掉;
- jweak NewWeakGlobalRef(JNIEnv *env, jobject obj):創建一個指向對象obj的弱全局引用,jweak是jobject的別名,如果obj是null則返回NULL,如果內存不足則拋出OutOfMemoryError異常;
- void DeleteWeakGlobalRef(JNIEnv *env, jweak obj):刪除弱全局引用;
- jobjectRefType GetObjectRefType(JNIEnv* env, jobject obj):獲取某個對象引用的引用類型,JDK1.6引入的。
(3)獲取jfieldID和jmethodID
在C/C++本地代碼中訪問Java端的代碼,一個常見的應用就是獲取類的字段和調用類的方法,為了在C/C++中表示字段和方法,JNI在jni.h頭文件中定義了jfieldId,jmethodID類型來分別代表Java端的字段和方法
我們在訪問或者設置Java字段的時候,首先就要先在本地代碼取得代表該Java字段的jfieldID,然后才能在本地代碼中進行Java屬性操作。同樣在調用Java端的方法時,也需要取得代表該方法的jmethodID才能進行Java方法調用。相關的函數如下:
- jfieldID GetFieldID(jclass clazz,const char* name,const char* sign),獲取實例字段的jfieldID;
- jfieldID GetStaticFieldID(jclass clazz, const char *name,const char *sig),獲取靜態字段的jfieldID;
- jmethodID GetMethodID(jclass clazz, const char *name,const char *sig),獲取實例方法的jmethodID;
- jmethodID GetStaticMethodID(jclass clazz, const char *name,const char *sig),獲取靜態方法的jmethodID。
更多關於JNI函數可參數:JNI Functions
2、加載和卸載本地方法
在編寫JNI對應的C/C++語言的實現時,還可以實現如下函數:
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved); JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved);
JVM提供了一種方式允許你在加載動態鏈接庫文件的時候做一些你想做的事情,也就是JNI_OnLoad()函數。顯式的卸載一個本地庫時會看到JNI_OnUnload()函數被調用。JNI_OnLoad()函數是在動態庫被加載時調用,而JNI_OnUnload()函數則是在本地庫被卸載時調用。所以這兩個函數就是一個本地庫最重要的兩個管理生命周期的函數。
可以用這一組函數實現Java方法和本地C函數的鏈接。舉個例子如下:
package com.classloading;
class NativeLib{ public static native String getName(int number); }
按照約定,需要在本地文件中有如下聲明:
JNIEXPORT jstring JNICALL Java_com_classloading_NativeLib_getName(JNIEnv *env,jobject thiz,jint number);
不過現在我們聲明了一個沒有按照JNI規范命名的本地函數,如下:
JNIEXPORT jstring JNICALL getName(JNIEnv *env, jclass clazz);
必須使用動態關聯的方式實現Java方法與本地函數的映射,代碼如下:
extern "C"
JNIEXPORT jstring JNICALL getName(JNIEnv *env, jobject thiz, int number) { printf("number is %d",number); return env->NewStringUTF("hello world"); } static const char *CLASS_NAME = "com/classloading/NativeLib"; // 類名 static JNINativeMethod method = { // 本地方法描述 "getName", // Java方法名 "(I)Ljava/lang/String;", // Java方法簽名 (void *) getName // 綁定到對應的本地函數 }; static bool bindNative(JNIEnv *env) { jclass clazz; clazz = env->FindClass(CLASS_NAME); if (clazz == NULL) { return false; } return env->RegisterNatives(clazz, &method, 1) == 0; } JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env = NULL; jint result = -1; if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { return result; } bool res = bindNative(env); printf("bind result is %s",res?"ok":"error"); // 返回JNI的版本 return JNI_VERSION_1_6; }
注意:JNI_OnLoad()函數在每一個動態鏈接庫中只能存在一個。常用javah去生成JNI的頭文件,然后嚴格按照頭文件中聲明的函數名稱等去實現自己的JNI函數,使用這種方式比較傳統,定義的格式甚至連名字都必須按照規范,不過JVM也同時提供了RegisterNative()函數手動的注冊native方法。調用的RegisterNatives()函數聲明如下:
jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,jint nMethods)
我們現在規范一下術語:
函數的解析如下:
1、methods 是一個二維數組(method需要取地址后就相當於二維數組),代表着這個clazz里的每一個native方法所對應的實現函數,在下面的例子中表示,一個native 方法retrieveDirectives,返回值為AssertionStatusDirectives,所對應的執行的本地函數是JVM_AssertionStatusDirectives。實例如下:
來源:/openjdk/jdk/src/share/native/java/lang/ClassLoader.c static JNINativeMethod methods[] = { {"retrieveDirectives", "()Ljava/lang/AssertionStatusDirectives;", (void *)&JVM_AssertionStatusDirectives} }; JNIEXPORT void JNICALL Java_java_lang_ClassLoader_registerNatives(JNIEnv *env, jclass cls) { (*env)->RegisterNatives(env, cls, methods, sizeof(methods)/sizeof(JNINativeMethod)); }
2、后面的nMethods代表要指定的native方法的數量
最后就是卸載native方法了,實現代碼如下:
static bool unBindNative(JNIEnv *env) { jclass clazz; clazz = env->FindClass(CLASS_NAME); if (clazz == NULL) { return false; } return env->UnregisterNatives(clazz) == 0; } JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved) { JNIEnv *env = NULL; jint result = -1; if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { return; } bool res = unBindNative(env); sprintf("unbind result is %s", res ? "ok" : "error"); }
3、JavaVM
JavaVM是虛擬機在JNI中的表示,一個JVM中只有一個JavaVM實例,這個實例是線程共享的。通過JNIEnv可以獲取一個Java虛擬機實例,其函數如下:
jint GetJavaVM(JNIEnv *env, JavaVM **vm);
vm用來存放獲得的虛擬機指針的指針,成功返回0,失敗返回其它值。
struct JNIInvokeInterface_; struct JavaVM_; #ifdef __cplusplus typedef JavaVM_ JavaVM; #else typedef const struct JNIInvokeInterface_ *JavaVM; #endif
可以看到C語言環境和C++語言環境中的實現是不一樣。JNIInvokeInterface_與JavaVM_的定義如下:
struct JNIInvokeInterface_ { void *reserved0; void *reserved1; void *reserved2; jint (JNICALL *DestroyJavaVM)(JavaVM *vm); jint (JNICALL *AttachCurrentThread)(JavaVM *vm, void **penv, void *args); jint (JNICALL *DetachCurrentThread)(JavaVM *vm); jint (JNICALL *GetEnv)(JavaVM *vm, void **penv, jint version); jint (JNICALL *AttachCurrentThreadAsDaemon)(JavaVM *vm, void **penv, void *args); }; struct JavaVM_ { const struct JNIInvokeInterface_ *functions; #ifdef __cplusplus jint DestroyJavaVM() { return functions->DestroyJavaVM(this); } jint AttachCurrentThread(void **penv, void *args) { return functions->AttachCurrentThread(this, penv, args); } jint DetachCurrentThread() { return functions->DetachCurrentThread(this); } jint GetEnv(void **penv, jint version) { return functions->GetEnv(this, penv, version); } jint AttachCurrentThreadAsDaemon(void **penv, void *args) { return functions->AttachCurrentThreadAsDaemon(this, penv, args); } #endif };
可以看到,JNIInvokeInterface_和JavaVM_的定義非常類似於JNINativeInterface_與JNIEnv_,其用法也非常類似,這里不再過多介紹。
公眾號 深入剖析Java虛擬機HotSpot 已經更新虛擬機源代碼剖析相關文章到60+,歡迎關注,如果有任何問題,可加作者微信mazhimazh,拉你入虛擬機群交流