下面介绍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,拉你入虚拟机群交流