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 #include <jni.h> #ifndef _Included_me_zhongmingmao_advanced_jni_Foo #define _Included_me_zhongmingmao_advanced_jni_Foo #ifdef __cplusplus extern "C" {#endif JNIEXPORT void JNICALL Java_me_zhongmingmao_advanced_jni_Foo_foo (JNIEnv *, jclass) ; JNIEXPORT void JNICALL Java_me_zhongmingmao_advanced_jni_Foo_bar__IJ (JNIEnv *, jobject, jint, jlong) ; 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
native方法对应的C函数都需要以Java_为前缀,之后跟着完整的 包名 、方法名 (和方法描述符 )
C函数名不支持/字符,/字符会被转换为_,原本方法名中的 _ 字符,转换为_1
当某个类出现重载的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 #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 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
JVM会将所有JNI函数的函数指针 聚合到一个名为JNIEnv 的数据结构中
JNIEnv是一个线程私有 的数据结构,JVM会为每个线程创建一个JNIEnv
并且规定C代码不能将当前线程的JNIEnv共享给其他线程,否则无法保证JNI函数的正确性
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 JNIEXPORT void JNICALL Java_me_zhongmingmao_advanced_jni_Foo_foo (JNIEnv *, jclass) ; JNIEXPORT void JNICALL Java_me_zhongmingmao_advanced_jni_Foo_bar__IJ (JNIEnv *, jobject, jint, jlong) ; JNIEXPORT void JNICALL Java_me_zhongmingmao_advanced_jni_Foo_bar__Ljava_lang_String_2Ljava_lang_Object_2 (JNIEnv *, jobject, jstring, jobject) ;
静态native方法foo接收两个参数
一个为JNIEnv 指针(聚合JNI函数的函数指针)
另一个是jclass 参数(用来指代定义该native方法的类 )
实例native方法bar的第二个参数为jobject 类型,用来指代该native方法的调用者
如果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) { 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)
当调用JNI函数的过程中,JVM会生成相关的异常实例 ,并缓存 在内存的某一个位置
但与Java编程不一样的是,它不会显式地跳转至异常处理器或者调用者,而是继续执行 接下来的C代码
因此,当从可能触发异常 的JNI函数返回时,需要通过JNI函数ExceptionOccurred 来检查是否发生了异常
如果无须抛出该异常,需要通过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) { 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); 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
句柄与性能 背景
在C代码 中,既可以访问所传入的引用类型参数 ,也可以通过JNI函数创建新的Java对象
这些Java对象 也会受到GC的影响 ,因此JVM需要一种机制,来告知GC算法:不要回收这些C代码中可能引用到的Java对象
该机制就是局部引用 和全局引用 ,GC算法会将这两种引用指向的对象标记为不可回收
局部引用与全局引用
局部引用
传入的引用类型参数 ,
通过JNI函数返回的引用类型参数 (除NewGlobalRef和NewWeakGlobalRef)
一旦从C函数返回至Java方法 之中,那么局部引用将失效
因此不能缓存局部引用 ,以供另一个C线程 或下一次native方法调用 时使用
因此,可以借助JNI函数NewGlobalRef ,将局部引用转换为全局引用 ,以确保其指向的Java对象不会被垃圾回收
相应的,可以通过JNI函数DeleteGlobalRef 来消除全局引用 ,以便回收被全局引用指向的Java对象
如果C函数运行时间极长 ,可以通过JNI函数DeleteLocalRef 来消除不再使用的局部引用 ,以便回收被引用的Java对象
句柄
由于垃圾回收器 可能会移动对象在内存中的位置 ,因此JVM需要另一种机制
保证局部引用 或全局引用 将正确地指向移动后的对象 ,HotSpot通过句柄 的方式来实现
句柄:Java对象指针的指针
当发生GC时,如果Java对象被移动了,那么句柄指向的指针也将发生变动,但句柄本身保持不变
无论局部引用 还是全局引用 ,都是句柄
局部引用所对应的句柄有两种存储方式
一种是在本地方法栈帧 中,主要用于存储C函数所接收的来自Java层面的引用类型参数
另一种是线程私有的句柄块 ,主要用于存储C函数运行过程中创建的局部引用
当从C函数返回至Java方法 时
本地方法栈帧中的句柄将被自动清除
线程私有句柄块则需要由JVM显式清除
JNI调用的额外性能开销
进入C函数时对引用类型参数的句柄化
调整参数位置 (C调用和Java调用传参的方式不一样)
从C函数返回时清理线程私有句柄块
参考资料 深入拆解Java虚拟机