JVM进阶 — 浅谈JNI

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

  1. native方法对应的C函数都需要以 Java_
    为前缀,之后跟着完整的 包名
    方法名
    (和 方法描述符
  2. C函数名不支持/字符,/字符会被转换为_,原本方法名中的 _ 字符,转换为_1
  3. 当某个类出现 重载的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

  1. JVM会将 所有JNI函数的函数指针
    聚合到一个名为 JNIEnv
    数据结构中
  2. JNIEnv是一个 线程私有
    的数据结构,JVM会为每个线程创建一个JNIEnv
    • 并且规定C代码不能将当前线程的JNIEnv共享给其他线程,否则 无法保证JNI函数的正确性
  3. 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);

  1. 静态native方法foo接收两个参数
    • 一个为 JNIEnv
      指针(聚合JNI函数的函数指针)
    • 另一个是 jclass
      参数(用来指代 定义该native方法的类
  2. 实例native方法bar的第二个参数为 jobject
    类型, 用来指代该native方法的调用者
  3. 如果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)

  1. 当调用JNI函数的过程中, JVM会生成相关的异常实例
    ,并 缓存
    在内存的某一个位置
  2. 但与Java编程不一样的是,它不会显式地跳转至异常处理器或者调用者,而是 继续执行
    接下来的C代码
  3. 因此,当从 可能触发异常
    的JNI函数返回时,需要通过JNI函数 ExceptionOccurred
    来检查是否发生了异常
  4. 如果无须抛出该异常,需要通过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

句柄与性能

背景

  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函数返回时 清理线程私有句柄块

转载请注明出处:http://zhongmingmao.me/2019/01/12/jvm-advanced-jni/

访问原文「JVM进阶 — 浅谈JNI」获取最佳阅读体验并参与讨论

原文 

http://zhongmingmao.me/2019/01/12/jvm-advanced-jni/

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » JVM进阶 — 浅谈JNI

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址