JNI
JNI是指Java原生接口,它允许在Java虚拟机中的运行的Java代码与其他编程语言(如C、C++、汇编)编写的程序和库进行互操作。它是一种调用规范,我们的编写的JNI程序可以运行在任何实现了该JNI规范的商业虚拟机上。
背景
尽快完全可以用Java编写应用程序,但是在某些情况下,仅仅Java无法满足应用程序的需求,这时需要本机方法来处理这些情况。
- 标准Java类库不支持应用程序所需的平台相关的功能
- 复用其他语言编写的库
- 使用较低级别的语言如汇编来实现对运行时间要求较高的代码
通过JNI,可以使用本机方法执行以下操作:
- 创建、检查和更新Java对象(包括数组和字符串)
- 调用Java方法
- 捕获并抛出异常
- 加载类并获取类信息
- 执行运行时类型检查
和其他技术一样,JNI也并不是一蹴而就的,在没有统一的JNI接口之前,各个厂商也都推出过自己的本机接口调用规范,如JDK1.0本机接口、Netscape提出的JRI、微软Java VM支持的本机接口。
后来大家认为统一的,经过深思熟虑的标准接口会为每个人带来好处。
- 每个VM厂商都可以支持更大范围的本机代码
- 工具构建者将不必维护不同种类的本机方法接口
- 应用程序程序员将能够编写其本地代码的一个版本,就可以在不同的VM上运行
JNI 定义了两个关键数据结构,即“JavaVM”和“JNIEnv”。两者本质上都是指向函数表的二级指针。
编译、加载、链接本地方法
因为Java虚拟机是支持多线程的,本机库也应该编译并与支持多线程的本机编译器链接。
两种链接方式:
- 动态链接 System.loadLibrary(“name”);
package pkg;
class Cls {
native double f(int i, String s);
static {
System.loadLibrary(“pkg_Cls”);
}
}
动态链接方式中,依据方法名称来分析调用入口。具体格式如下:
- 以Java_开头
- 类名的完整限定符
- 用下划线_分割
- 方法名称,如Java_pkg_Cls_f
- 对于重载的本机方法,后面加两个下划线
- 静态链接 RegisterNatives()
public class Object {
private static native void registerNatives();
static {
registerNatives();
}
}
JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
(*env)->RegisterNatives(env, cls,
methods, sizeof(methods)/sizeof(methods[0]));
}
上面registerNatives()是通过动态链接形式调用的,在其本地方法内部又通过RegisterNatives函数静态注册了一些函数methods,methods定义如下:
static JNINativeMethod methods[] = {
{"hashCode", "()I", (void *)&JVM_IHashCode},
{"wait", "(J)V", (void *)&JVM_MonitorWait},
{"notify", "()V", (void *)&JVM_MonitorNotify},
{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};
本机方法参数
本地方法的第一个参数固定为JNIEnv类型的指针。
第二参数区分静态方法或非静态方法而有所不同。静态方法的第二参数是其Java类的引用,非静态方法的第二个参数是对对象的引用。
之后就是原Java方法中对应的参数。
下面是非静态方法的示例:
JNIEXPORT jclass JNICALL
Java_java_lang_Object_getClass(JNIEnv *env, jobject this)
{
if (this == NULL) {
JNU_ThrowNullPointerException(env, NULL);
return 0;
} else {
return (*env)->GetObjectClass(env, this);
}
}
全局引用和本地引用
JNI将本机代码使用的对象引用分为两类:本地引用和全局引用。
本地引用在本机方法调用期间有效,并在本机方法返回后自动释放。
全局引用在显示释放之前一直有效。
Java对象指针作为本地引用传递给本机方法,JNI函数返回的所有Java对象都是本地引用。本地引用只在当前线程有效,不能传递到另一个线程使用。
本地方法访问Java对象的方法和字段
JNI允许本地方法访问字段并调用Java对象的方法。JNI通过他们的符号名和类型签名来标识方法和字段。
例如要调用类cls中的方法f,先获取方法ID,如下所示:
jmethodID mid = env->GetMethodID(cls, “f”, "(ILJava/lang/String;)D");
然后本地代码可以重复使用方法ID,从而无需多次花费茶查找方法的时间,调用方式如下:
jdouble result = env->CallDoubleMethod(obj, mid, 10, str);
注意:字段或方法ID不会阻止VM卸载已经获取方法ID的类,卸载类后,方法或字段ID变为无效。
如果打算长时间使用方法或字段ID,需要:
- 保持对基础类的实时引用
- 每次都重新计算方法或字段ID
JNI类型和数据结构
下表中描述了Java基本数据类型和与其对应的设备相关的本地数据类型:
Java类型 | 本地类型 | 描述 |
---|---|---|
boolean | jboolean | 无符号8位 |
byte | jbyte | 有符号8位 |
char | jchar | 无符号16位 |
short | jshort | 有符号16位 |
int | jint | 有符号32位 |
long | jlong | 有符号64位 |
float | jfloat | 32位 |
double | jdouble | 64位 |
void | void | N/A |
JNI包含了许多引用类型,对应了不同种类的Java对象类型。
jobject | 所有Java对象 | ||
---|---|---|---|
jclass | Class对象 | ||
jstring | String对象 | ||
jarray | 数组对象 | ||
jobjectArray | 对象数组 | ||
jbooleanArray | 布尔数组 | ||
jbyteArray | 字节数组 | ||
jcharArray | 字符数组 | ||
jshortArray | 短整型数组 | ||
jintArray | 整型数组 | ||
jlongArray | 长整型数组 | ||
jfloatArray | 浮点型数组 | ||
jdoubleArray | 双精度浮点型数组 | ||
jthrowable | Throwable对象 |
类型签名
JNI使用Java虚拟机的类型签名方式来标识一个类型和方法。
类型签名 | Java类型 |
---|---|
Z | boolean |
B | byte |
C | char |
S | Short |
I | int |
J | long |
F | float |
D | doubble |
L full-qualified-class; | 类型的全限定符 |
[ type | 某种类型type的数组 |
(参数类型)返回值类型 | 方法类型 |
下面为一个示例:
long f(int n, String s, int[] arr);
上述方法的类型签名为:
(ILjava/lang/String;[I)J
接口函数表
所有JNI定义的函数都被包含到了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);
...
}
//接口函数表实现
struct JNINativeInterface_ jni_NativeInterface = {
NULL,
NULL,
NULL,
NULL,
jni_GetVersion,
jni_DefineClass,
jni_FindClass,
...
}
在接口函数表中的前三个函数作为保留项,是为了将来适配COM。第四个函数作为保留项是为了将来再新定义类相关的JNI操作时可以被添加到FindClass函数后面,而不是添加到函数表的末尾。
下面是jni_GetVersion函数的实现:
JNI_LEAF(jint, jni_GetVersion(JNIEnv *env))
JNIWrapper("GetVersion");
#ifndef USDT2
DTRACE_PROBE1(hotspot_jni, GetVersion__entry, env);
#else /* USDT2 */
HOTSPOT_JNI_GETVERSION_ENTRY(
env);
#endif /* USDT2 */
#ifndef USDT2
DTRACE_PROBE1(hotspot_jni, GetVersion__return, CurrentVersion);
#else /* USDT2 */
HOTSPOT_JNI_GETVERSION_RETURN(
CurrentVersion);
#endif /* USDT2 */
return CurrentVersion;
JNI_END
我们前面说到的本地方法的第一个参数,固定为JNIEnv类型的指针,这个JNIEnv类型定义如下:
struct JNIEnv_;
#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif
如果是C语言环境,JNIEnv指针就是JniNativeInterface类型。
如果是C++语言环境,JNIEnv对象是JNIEnv_类型的实例,JNIEnv_类型定义如下:
struct JNIEnv_ {
const struct JNINativeInterface_ *functions;
#ifdef __cplusplus
jint GetVersion() {
return functions->GetVersion(this);
}
...
}
可以看出,其内部就是对JNINativeInterface_结构体的封装。
综上所述,JNIEnv就是一系列JNI函数的集合。
The Invocation API
Invocation API允许在在本地方法中创建一个Java虚拟机实例。如下所示:
#include <jni.h> /* where everything is defined */
...
JavaVM *jvm; /* JavaVM代表一个虚拟机实例 */
JNIEnv *env; /* JNI接口函数表 */
JavaVMInitArgs vm_args; /* JDK/JRE 6 虚拟机初始化参数 */
JavaVMOption* options = new JavaVMOption[1];
options[0].optionString = "-Djava.class.path=/usr/lib/java";
vm_args.version = JNI_VERSION_1_6;
vm_args.nOptions = 1;
vm_args.options = options;
vm_args.ignoreUnrecognized = false;
/* load and initialize a Java VM, return a JNI interface
* pointer in env */
/* 加载并初始化一个Java虚拟机,env是一个返回参数*/
JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
delete options;
/* 使用JNIEnv调用Main类的test方法 */
jclass cls = env->FindClass("Main");
jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V");
env->CallStaticVoidMethod(cls, mid, 100);
jvm->DestroyJavaVM();
其中JavaVM定义如下:
struct JavaVM_;
#ifdef __cplusplus
typedef JavaVM_ JavaVM;
#else
typedef const struct JNIInvokeInterface_ *JavaVM;
#endif
//JNIInvokeInterface_定义
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);
};
//JNIInvokeInterface_实现
const struct JNIInvokeInterface_ jni_InvokeInterface = {
NULL,
NULL,
NULL,
jni_DestroyJavaVM,
jni_AttachCurrentThread,
jni_DetachCurrentThread,
jni_GetEnv,
jni_AttachCurrentThreadAsDaemon
};
从上面可以看出,JavaVM类型就是一个指向Invocation API函数表的指针。大家可能注意到了,有三个Invocation API:JNI_GetDefaultJavaVMInitArgs()、JNI_GetCreateJavaVMs()和JNI_CreateJavaVM()不在JavaVM函数表中。是因为这三个函数可以在没有JavaVM结构的情况下使用。