JNI学习之Invocation API
本文是对链接http://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html的学习笔记,限于英文水平和对JNI的理解,可能存在错误。
简介
通过使用Invocation API,使用C/C++开发的本地应用可以访问Java虚拟机提供的特性。为了描述简单,下面提到的VM指的都是Java虚拟机。
创建VM
在本地应用里,调用JNI_CreateJavaVM()方法可以完成初始化、加载VM,并返回指向新VM对象的一个指针。调用JNI_CreateJavaVM方法的线程,被称为主线程。
线程与VM的关联操作
JNIEnv对象并不是线程安全的,因此只能在当前线程使用。当需要跨线程使用JNIEnv对象时,需要通过调用AttachCurrentThread方法将当前线程与JVM进行关联,并得到一个指向JNIEnv对象的指针。当AttachCurrentThread方法调用成功之后,当前本地线程即可被VM感知。由于本地线程并不由JVM创建,因而需要确保自身有足够的栈空间来执行必要的代码。
调用者可以在本地线程中调用DetachCurrentThread来解除关联关系,以便于释放资源,否则会导致资源泄漏;但在本地线程的调用栈内仍有Java方法时,调用DetachCurrentThread方法可能会失败。
卸载VM
当VM使用完毕,就应当考虑停止VM并回收资源,通过调用JNI_DestroyJavaVM方法即可达到这一目的。在VM看来,用户线程包括VM在执行Java字节码时创建的Java线程,以及通过调用AttachCurrentThread方法进而与VM完成关联的本地线程。用户线程的代码在执行时,可能会持有比如锁、窗口之类的系统资源,为了简化VM的实现,VM把释放资源的操作留给程序员去做,VM要求调用JNI_DestroyJavaVM方法的当前线程必须是当前唯一存活的用户线程,否则JNI_DestroyJavaVM方法调用后可能无法达到预期的效果。
动态库和版本管理
本地动态库被VM加载之后,对于VM内部所有的类加载器都是可见的。即VM内部由不同类加载器加载的两个类可以关联到相同的本地方法,这带来两个问题:
- 一个Java类可能会与由另外一个类加载触发加载的本地库建立关联关系;
- 本地方法无法区分当前的调用是来自VM内部由不同类加载器加载的哪个类,这破坏了由类加载器控制的类命名空间,从而可能引入类型安全相关的问题;
为了解决上述的两个问题,引入了新的解决方法,即某个类加载器自己管理当前加载的本地库的集合,并且相同的本地库只能被一个类加载器加载。应用的代码违反这两点约束将导致UnsatisfiedLinkError的出现。
新方法的优点有:
- 基于类加载器实现的命名空间管理在本地库的使用方面得到了保留,本地库可以无需考虑来自不同类加载器的类的调用;
- 当加载某个本地库的类加载器被GC掉之后,本地库也可以自动被释放掉资源。
JNI_OnLoad
为了便于实现上述特性,VM暴露了方法JNI_OnLoad。在VM加载本地库时,VM会自动在本地库文件中查找这个方法,如果方法存在则通过这个方法来获取本地库使用的JNI版本号,这样VM可以决定本地库使用VM特性的请求是否合理。如果JNI_OnLoad方法没有实现,VM认为本地库基于JNI_VERSION_1_1相关的特性实现。如果VM无法识别JNI_OnLoad方法的返回值,VM会忽略本地库的加载请求,并清理现场。
JNI_OnUnLoad
当加载本地库的类加载器被GC之后,VM会主动调用本地库导出的JNI_OnUnLoad方法,如果本地库没定义这个方法的话,这个步骤将自动忽略。一般而言,可以在JNI_OnUnLoad方法内部做一些清理操作。由于JNI_OnUnLoad方法被VM回调的时机不确定,因而要避免在这个方法内部调用Java语言的方法以及VM提供的特性。
Invocation API简介
这一章节提到的API均由VM提供。方法的返回值为JNI_OK 时表示调用成功,非JNI_OK 表示调用失败。
如下是常用的几个结构定义。
typedef struct JavaVMInitArgs { jint version; jint nOptions; JavaVMOption *options; jboolean ignoreUnrecognized; } JavaVMInitArgs; typedef struct JavaVMOption { char *optionString; /* the option as a string in the default platform encoding */ void *extraInfo; } JavaVMOption; typedef struct JavaVMAttachArgs { jint version; char *name; /* the name of the thread as a modified UTF-8 string, or NULL */ jobject group; /* global ref of a ThreadGroup object, or NULL */ } JavaVMAttachArgs;
JNI_GetDefaultJavaVMInitArgs
签名为jint JNI_GetDefaultJavaVMInitArgs(void *vm_args);通过调用本方法,可以得到VM的默认配置属性。方法入参为指向JavaVMInitArgs类型对象的指针,在本地代码调用JNI_GetDefaultJavaVMInitArgs方法前需要设置期望VM支持的版本号。方法调用返回值为JNI_OK时表示成功,VM会将版本号设置为实际支持的值;方法的返回值非JNI_OK时,表示调用失败。
JNI_GetCreatedJavaVMs
签名为jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs);通过调用本方法,可以获取到当前已创建的全部VM对象。vmBuf为保存指针的数组,长度由bufLen给出,而实际的VM数量将在nVMs变量中返回。
单个进程中不允许创建多个VM实例。
JNI_CreateJavaVM
签名为jint JNI_CreateJavaVM(JavaVM **p_vm, void **p_env, void *vm_args);创建VM对象,并完成必要的初始化操作,同时调用方法的线程被设置为主线程。在单个进程中不允许创建多个VM对象。
DestroyJavaVM
签名为jint DestroyJavaVM(JavaVM *vm);通过调用本方法,可以卸载已创建的VM对象,并回收相关的资源。本方法是线程安全的,可以在任意线程使用。本方法在使用时会阻塞当前线程吗?假如调用线程没有与VM对象建立了关联关系,则直接建立关联关系,然后等待其它用户线程退出;假如已建立了关联关系,则直接等待其它用户线程退出。
Unloading of the VM is not supported.这语没有看明白什么意思。
AttachCurrentThread
签名jint AttachCurrentThread(JavaVM *vm, void **p_env, void *thr_args);当前调用线程与VM建立关联关系,获取到可在当前线程安全使用的JNIEnv对象。假如当前线程已经与VM建立关联,多次调用本方法是安全的。本地线程在同一时段内,只能与一个VM对象建立关联关系(这个说法很奇怪,之前的资源提示在单个进程内,只允许创建一个VM,这里为什么又提示说避免与多个VM关联?)。
当本地线程与VM建立关联之后,线程使用的上下文类加载器将是VM的启动类加载器。
调用本方法时,第一个参数为指向VM对象的指针,第二个参数为JNIEnv类型对象的指针,第三个参数为JavaVMAttachArgs类型对象的指针,但没有实际用途,应当为设置为NULL(不过原文档对这个参数的介绍稍有点混乱,需要实际验证一下)。
AttachCurrentThreadAsDaemon
签名jint AttachCurrentThreadAsDaemon(JavaVM* vm, void** penv, void* args);用法和参数与AttachCurrentThread方法类似,区别在于VM内部创建的java.lang.Thread对象将会是一个daemon。如果当前本地线程已经和VM建立关联,则多次调用AttachCurrentThread或者AttachCurrentThreadAsDaemon并不会修改java.lang.Thread对象的daemon属性。
DetachCurrentThread
签名jint DetachCurrentThread(JavaVM *vm);当前线程与VM解除关联,本地线程持有的锁对象将全部释放。等待当前线程的Java线程将得到通知。主线程通过调用本方法,可以和VM解除关联。
GetEnv
签名为jint GetEnv(JavaVM *vm, void **env, jint version);通过调用本方法,可以获取到当前线程的JNIEnv对象。如果当前线程与VM没有建立关联,则*env被设置为NULL,同时返回JNI_EDETACHED;如果传入的VM特性版本号不被支持,则*env被设置为NULL,同时返回JNI_EVERSION;如果调用成功,则*env被设置为正确的JNIEnv对象指针,同时返回JNI_OK。