native方法

1
2
3
public class Object {
public native int hashCode();
}

当Java代码调用native方法时,JVM将通过JNI,调用至对应的C函数
Object.hashCode()就是一个native方法,对应的C函数将计算对象的哈希值,并缓存在对象头栈上锁记录(轻量级锁)或者对象监视锁(重量级锁,monitor)中,以确保该值在对象的生命周期之内不会变更

链接方式

在调用native方法之前,JVM需要将该native方法链接至对应的C函数上

自动链接

JVM自动查找符合默认命名规范的C函数,并且链接起来

Java代码

1
2
3
4
5
6
7
8
package me.zhongmingmao.advanced.jni;

public class Foo {
int i = 0xDEADBEEF;
public static native void foo();
public native void bar(int i, long j);
public native void bar(String s, Object o);
}

生成C头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$ javac -h . me/zhongmingmao/advanced/jni/Foo.java

$ cat me_zhongmingmao_advanced_jni_Foo.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class me_zhongmingmao_advanced_jni_Foo */

#ifndef _Included_me_zhongmingmao_advanced_jni_Foo
#define _Included_me_zhongmingmao_advanced_jni_Foo
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: me_zhongmingmao_advanced_jni_Foo
* Method: foo
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_me_zhongmingmao_advanced_jni_Foo_foo
(JNIEnv *, jclass);

/*
* Class: me_zhongmingmao_advanced_jni_Foo
* Method: bar
* Signature: (IJ)V
*/
JNIEXPORT void JNICALL Java_me_zhongmingmao_advanced_jni_Foo_bar__IJ
(JNIEnv *, jobject, jint, jlong);

/*
* Class: me_zhongmingmao_advanced_jni_Foo
* Method: bar
* Signature: (Ljava/lang/String;Ljava/lang/Object;)V
*/
JNIEXPORT void JNICALL Java_me_zhongmingmao_advanced_jni_Foo_bar__Ljava_lang_String_2Ljava_lang_Object_2
(JNIEnv *, jobject, jstring, jobject);

#ifdef __cplusplus
}
#endif
#endif
  1. native方法对应的C函数都需要以Java_为前缀,之后跟着完整的包名方法名(和方法描述符
  2. C函数名不支持/字符,/字符会被转换为_,原本方法名中的 _ 字符,转换为_1
  3. 当某个类出现重载的native方法时,JVM会将参数类型纳入自动链接对象的考虑范围之中
    • 在前面C函数名的基础上,追加__以及方法描述符作为后缀
    • 方法描述符中的特殊符号同样会被替换:
      • 分隔符/被替换为_
      • 引用类型所使用的;被替换为_2
      • 数组类型所使用的[被替换为_3

主动链接

这种链接方式对C函数名没有要求,通常会使用一个名为registerNatives的native方法,该方法还是会按照自动链接的方式链接到对应的C函数,然后在registerNatives对应的C函数中,手动链接该类的其他native方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Object {
// 自动链接
private static native void registerNatives();
static {
registerNatives();
}
public final native Class<?> getClass();

// 主动链接
public native int hashCode();
public final native void wait(long timeout) throws InterruptedException;
public final native void notify();
public final native void notifyAll();
protected native Object clone() throws CloneNotSupportedException;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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},
};

JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
(*env)->RegisterNatives(env, cls,
methods, sizeof(methods)/sizeof(methods[0]));
}

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);
}
}

C函数将调用RegisterNatives API,注册Object类中其他native方法(不包括getClass)所要链接的C函数,这些C函数的函数名并不符合默认的命名规则,详细的C代码请查阅Object.c

实现native方法

C实现

1
2
3
4
5
6
7
8
9
// foo.c
#include <stdio.h>
#include "me_zhongmingmao_advanced_jni_Foo.h"

JNIEXPORT void JNICALL Java_me_zhongmingmao_advanced_jni_Foo_bar__Ljava_lang_String_2Ljava_lang_Object_2
(JNIEnv *env, jobject thisObject, jstring str, jobject obj) {
printf("Hello, World\n");
return;
}

动态链接库

通过gcc命令将其编译成动态链接库,动态链接库的名字必须以lib为前缀,以**.dylib(Linux上为.so**)为扩展名

1
$ gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/darwin -o libfoo.dylib -shared foo.c

调用

1
2
3
4
5
6
7
8
9
10
// -Djava.library.path=$PATH_TO_DYLIB
public static void main(String[] args) {
try {
System.loadLibrary("foo");
} catch (UnsatisfiedLinkError e) {
e.printStackTrace();
System.exit(1);
}
new Foo().bar("", "");
}
1
2
$ java -Djava.library.path=$PATH_TO_DYLIB me.zhongmingmao.advanced.jni.Foo
Hello, World

JNI API

  1. JVM会将所有JNI函数的函数指针聚合到一个名为JNIEnv的数据结构中
  2. JNIEnv是一个线程私有的数据结构,JVM会为每个线程创建一个JNIEnv
    • 并且规定C代码不能将当前线程的JNIEnv共享给其他线程,否则无法保证JNI函数的正确性
  3. JNIEnv采用线程私有的设计原因
    • JNI函数提供一个单独的命名空间
    • 允许JVM通过更改函数指针来的方式来替换JNI函数的具体实现

类型映射关系

JNI会将Java层面的基本类型以及引用类型映射为另一套可供C代码使用的数据结构

基本类型

1
2
3
4
5
6
7
8
9
10
11
Java类型     C数据结构
--------------------
boolean jboolean
byte jbyte
char jchar
short jshort
int jint
long jlong
float jfloat
double jdouble
void jvoid

引用类型

引用类型对应的数据结构之间也存在继承关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
jobject
|- jclass (java.lang.Class objects)
|- jstring (java.lang.String objects)
|- jthrowable (java.lang.Throwable objects)
|- jarray (arrays)
|- jobjectArray (object arrays)
|- jbooleanArray (boolean arrays)
|- jbyteArray (byte arrays)
|- jcharArray (char arrays)
|- jshortArray (short arrays)
|- jintArray (int arrays)
|- jlongArray (long arrays)
|- jfloatArray (float arrays)
|- jdoubleArray (double arrays)

头文件解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* Class: me_zhongmingmao_advanced_jni_Foo
* Method: foo
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_me_zhongmingmao_advanced_jni_Foo_foo
(JNIEnv *, jclass);

/*
* Class: me_zhongmingmao_advanced_jni_Foo
* Method: bar
* Signature: (IJ)V
*/
JNIEXPORT void JNICALL Java_me_zhongmingmao_advanced_jni_Foo_bar__IJ
(JNIEnv *, jobject, jint, jlong);

/*
* Class: me_zhongmingmao_advanced_jni_Foo
* Method: bar
* Signature: (Ljava/lang/String;Ljava/lang/Object;)V
*/
JNIEXPORT void JNICALL Java_me_zhongmingmao_advanced_jni_Foo_bar__Ljava_lang_String_2Ljava_lang_Object_2
(JNIEnv *, jobject, jstring, jobject);
  1. 静态native方法foo接收两个参数
    • 一个为JNIEnv指针(聚合JNI函数的函数指针)
    • 另一个是jclass参数(用来指代定义该native方法的类
  2. 实例native方法bar的第二个参数为jobject类型,用来指代该native方法的调用者
  3. 如果native方法声明了参数,那么对应的C函数也将会接收这些参数(映射为对应的C数据结构)

获取实例字段

修改C代码,获取Foo类实例的i字段

1
2
3
4
5
6
7
8
9
JNIEXPORT void JNICALL Java_me_zhongmingmao_advanced_jni_Foo_bar__Ljava_lang_String_2Ljava_lang_Object_2
(JNIEnv *env, jobject thisObject, jstring str, jobject obj) {
// JNI中访问实例字段的方式类似于JAVA的反射API
jclass cls = (*env)->GetObjectClass(env, thisObject);
jfieldID fieldID = (*env)->GetFieldID(env, cls, "i", "I");
jint value = (*env)->GetIntField(env, thisObject, fieldID);
printf("Hello, World 0x%x\n", value);
return;
}
1
2
$ java -Djava.library.path=$PATH_TO_DYLIB me.zhongmingmao.advanced.jni.Foo
Hello, World 0xdeadbeef

如果尝试获取不存在的实例字段j,会抛出异常

1
2
3
4
5
$ java -Djava.library.path=$PATH_TO_DYLIB me.zhongmingmao.advanced.jni.Foo
Hello, World 0x1
Exception in thread "main" java.lang.NoSuchFieldError: j
at me.zhongmingmao.advanced.jni.Foo.bar(Native Method)
at me.zhongmingmao.advanced.jni.Foo.main(Foo.java:19)
  1. 当调用JNI函数的过程中,JVM会生成相关的异常实例,并缓存在内存的某一个位置
  2. 但与Java编程不一样的是,它不会显式地跳转至异常处理器或者调用者,而是继续执行接下来的C代码
  3. 因此,当从可能触发异常的JNI函数返回时,需要通过JNI函数ExceptionOccurred来检查是否发生了异常
  4. 如果无须抛出该异常,需要通过JNI函数ExceptionClear显式地清空已缓存的异常实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
JNIEXPORT void JNICALL Java_me_zhongmingmao_advanced_jni_Foo_bar__Ljava_lang_String_2Ljava_lang_Object_2
(JNIEnv *env, jobject thisObject, jstring str, jobject obj) {
// JNI中访问实例字段的方式类似于JAVA的反射API
jclass cls = (*env)->GetObjectClass(env, thisObject);
jfieldID fieldID = (*env)->GetFieldID(env, cls, "j", "I");
if((*env)->ExceptionOccurred(env)) {
printf("Exception!\n");
(*env)->ExceptionClear(env);
}
fieldID = (*env)->GetFieldID(env, cls, "i", "I");
jint value = (*env)->GetIntField(env, thisObject, fieldID);
// we should put an exception guard here as well.
printf("Hello, World 0x%x\n", value);
return;
}
1
2
3
$ java -Djava.library.path=$PATH_TO_DYLIB me.zhongmingmao.advanced.jni.Foo
Exception!
Hello, World 0xdeadbeef

句柄与性能

背景

  1. C代码中,既可以访问所传入的引用类型参数,也可以通过JNI函数创建新的Java对象
  2. 这些Java对象也会受到GC的影响,因此JVM需要一种机制,来告知GC算法:不要回收这些C代码中可能引用到的Java对象
  3. 该机制就是局部引用全局引用,GC算法会将这两种引用指向的对象标记为不可回收

局部引用与全局引用

  1. 局部引用
    • 传入的引用类型参数
    • 通过JNI函数返回的引用类型参数(除NewGlobalRef和NewWeakGlobalRef)
  2. 一旦从C函数返回至Java方法之中,那么局部引用将失效
    • 因此不能缓存局部引用,以供另一个C线程下一次native方法调用时使用
    • 因此,可以借助JNI函数NewGlobalRef,将局部引用转换为全局引用,以确保其指向的Java对象不会被垃圾回收
    • 相应的,可以通过JNI函数DeleteGlobalRef来消除全局引用,以便回收被全局引用指向的Java对象
  3. 如果C函数运行时间极长,可以通过JNI函数DeleteLocalRef来消除不再使用的局部引用,以便回收被引用的Java对象

句柄

  1. 由于垃圾回收器可能会移动对象在内存中的位置,因此JVM需要另一种机制
    • 保证局部引用全局引用正确地指向移动后的对象,HotSpot通过句柄的方式来实现
    • 句柄:Java对象指针的指针
    • 当发生GC时,如果Java对象被移动了,那么句柄指向的指针也将发生变动,但句柄本身保持不变
  2. 无论局部引用还是全局引用,都是句柄
  3. 局部引用所对应的句柄有两种存储方式
    • 一种是在本地方法栈帧中,主要用于存储C函数所接收的来自Java层面的引用类型参数
    • 另一种是线程私有的句柄块,主要用于存储C函数运行过程中创建的局部引用
  4. 从C函数返回至Java方法
    • 本地方法栈帧中的句柄将被自动清除
    • 线程私有句柄块则需要由JVM显式清除
  5. JNI调用的额外性能开销
    • 进入C函数时对引用类型参数的句柄化
    • 调整参数位置(C调用和Java调用传参的方式不一样)
    • 从C函数返回时清理线程私有句柄块

参考资料

深入拆解Java虚拟机