转载

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/
正文到此结束
Loading...