内容简介:当Java代码调用native方法时,JVM将通过Object.hashCode()就是一个native方法,对应的C函数将计算对象的哈希值,并缓存在在调用native方法之前,JVM需要将该native方法链接至对应的C函数上
public class Object { public native int hashCode(); }
当 Java 代码调用native方法时,JVM将通过 JNI ,调用至对应的C函数
Object.hashCode()就是一个native方法,对应的C函数将计算对象的哈希值,并缓存在 对象头 、 栈上锁记录 (轻量级锁)或者 对象监视锁 (重量级锁,monitor)中,以确保该值在 对象的生命周期之内不会变更
链接方式
在调用native方法之前,JVM需要将该native方法链接至对应的C函数上
自动链接
JVM自动查找符合 默认命名规范 的C函数,并且链接起来
Java代码
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头文件
$ 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
- native方法对应的C函数都需要以 Java_ 为前缀,之后跟着完整的 包名 、 方法名 (和 方法描述符 )
- C函数名不支持/字符,/字符会被转换为_,原本方法名中的 _ 字符,转换为_1
-
当某个类出现 重载的native方法
时,JVM会将 参数类型
纳入自动链接对象的考虑范围之中
- 在前面C函数名的基础上,追加__以及 方法描述符 作为后缀
-
方法描述符中的 特殊符
号同样会被替换:
- 分隔符/被替换为_
- 引用类型所使用的;被替换为_2
- 数组类型所使用的[被替换为_3
主动链接
这种链接方式对C函数名没有要求,通常会使用一个名为 registerNatives
的native方法,该方法还是会按照 自动链接
的方式链接到对应的C函数,然后在 registerNatives
对应的C函数中, 手动链接该类的其他native方法
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; }
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实现
// 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 )为扩展名
$ gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/darwin -o libfoo.dylib -shared foo.c
调用
// -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("", ""); }
$ 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代码使用的 数据结构
基本类型
Java类型 C数据结构 -------------------- boolean jboolean byte jbyte char jchar short jshort int jint long jlong float jfloat double jdouble void jvoid
引用类型
引用类型对应的数据结构之间也存在 继承 关系
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)
头文件解析
/* * 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);
-
静态native方法foo接收两个参数
- 一个为 JNIEnv 指针(聚合JNI函数的函数指针)
- 另一个是 jclass 参数(用来指代 定义该native方法的类 )
- 实例native方法bar的第二个参数为 jobject 类型, 用来指代该native方法的调用者
- 如果native方法声明了参数,那么对应的C函数也将会接收这些参数(映射为对应的C数据结构)
获取实例字段
修改C代码,获取Foo类实例的i字段
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; }
$ java -Djava.library.path=$PATH_TO_DYLIB me.zhongmingmao.advanced.jni.Foo Hello, World 0xdeadbeef
如果尝试获取 不存在 的实例字段j,会抛出异常
$ 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 显式地 清空已缓存的异常实例
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; }
$ 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函数返回时 清理线程私有句柄块
转载请注明出处:http://zhongmingmao.me/2019/01/12/jvm-advanced-jni/
访问原文「JVM进阶 -- 浅谈JNI」获取最佳阅读体验并参与讨论
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
离散数学及其应用(原书第4版)
Kenneth H.Rosen / 机械工业出版社 / 2002-1-1 / 75.00
离散数学及其应用:原书第4版,ISBN:9787111075776,作者:(美)Kenneth H.Rosen著;袁崇义[等]译一起来看看 《离散数学及其应用(原书第4版)》 这本书的介绍吧!