JNI动态注册、静态注册实例及其实现原理分析


前言

想要彻底学好JVM需要一定C语言基础,要理解头文件、指针、结构体、结构体指针、函数指针、类型别名等概念,本文尽量通俗易懂的介绍JNI,语雀地址:https://www.yuque.com/yangxiaofei-vquku/wmp1zm/dmz2gd

一、什么是JNI

JNI全名Java Native Interface中文名java本地开发接口。通过JNI可以让java代码调用C或C++等底层语言,也可以用C或C++调用java语言,是java与其他语言交互的协议和桥梁,可以保证java代码的可移植性。

二、为什么要用JNI

当你无法用纯Java来实现需求的时候,就需要使用JNI来用底层语言编写的本地方法来满足这些该需求。列如:
当java库我发满足某些特殊平台的功能时。
已经用其他语言写好了库,不想重写编码想通过java代码直接调用时
希望实现一部分时间和性能要求都很高的逻辑,需要用底层语言来编写,如视频图片处理,游戏逻辑等。

三、怎么用JNI

1、JNI的静态注册

1)流程

  • 编写java代码调用Native方法sayHello()
    在这里插入图片描述

  • 使用javah -jni jvm.jni.StaticDemo 命令生成.h的头文件(在c语言中头文件可以理解问java中的interface接口)
    在这里插入图片描述

  • 使用C语言开发工具(我用的CLion)创建一个C语言工程并将新生成的.h文件copy到工程目录中,然后将jre/include/jni.h、jre/include/darwin/jni_md.h两个文件copy到C语言的工程目录中,最后创建jvm_jni_StaticDemo.c文件。

在这里插入图片描述
在这里插入图片描述

  • 在jvm_jni_StaticDemo.c文件中编写Java_jvm_jni_StaticDemo_sayHello()方法的实现逻辑
    在这里插入图片描述

  • 在CMakeLists.txt添加add_library(staticDemoLib SHARED jvm_jni_StaticDemo.h jvm_jni_StaticDemo.c)然后点击buildproject编译工程
    在这里插入图片描述

  • 编译完之后在make-build-debug文件夹下找到编译好的动态库文件libstaticDemoLib.dylib(mac为dylib,windows为dll,linux为so),将其放置在环境变量CLASSPATH的任意路径下都可以,本人放置在%JAVA_HOME%/lib的目录下(对应windows系统的%JAVA_HOME%/jre/bin目录)。
    在这里插入图片描述

在这里插入图片描述

  • 在java代码中添加静态代码块static{System.loadLibrary(“staticDemoLib”);}加载动态库,并运行代码
    在这里插入图片描述

2)原理

静态注册的原理是当加载动态库到jvm后当Native方法第一次执行时会根据其方法名去匹配对应的C语言实现命名规则解释可参考官方文档(我们无需手写,用javah命令生成即可)https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/design.html#wp16696
在这里插入图片描述

JNI静态注册实现的缺点:

  1. 后期类名、文件名改动,头文件所有函数将失效,需要手动改,超级麻烦易出错
  2. 代码编写不方便,由于JNI层函数的名字必须遵循特定的格式,且名字特别长;
  3. 会导致程序员的工作量很大,因为必须为所有声明了native函数的java类编写JNI头文件;
  4. 程序运行效率低,因为初次调用native函数时需要根据根据函数名在JNI层中搜索对应的本地函数,然后建立对应关系,这个过程比较耗时。
    总结:缺点显而易见不推荐正式环境使用,只用于学习理解。

2、JNI的动态注册

1)流程

  • 编写java代码调用Native方法sayHello(),sayBye();
    -在这里插入图片描述

  • 创建DynamicDemo.c(任意命名)文件,并编写sayHello、sayBye的实现,添加JNI_OnLoad的实现

#include "jni.h"
/**
 *  sayHello方法的实现
 * @param jniEnv JNINativeInterface_结构体的二级指针对象里面包含了很多函数指针
 * @param jobject1 调用此方法的对象实例(即main方法中的dynamicDemo)
 * @return
 */
jstring fun1(JNIEnv *jniEnv, jobject jobject1){
    return (*jniEnv)->NewStringUTF(jniEnv,"我是Native方法sayHello的实现你好");
};
/**
 *  sayBye方法的实现
 * @param jniEnv  JNINativeInterface_结构体的二级指针对象里面包含了很多函数指针
 * @param jobject1 调用此方法的对象实例(即main方法中的dynamicDemo)
 * @return
 */
jstring fun2(JNIEnv *jniEnv, jobject jobject1){
    return (*jniEnv)->NewStringUTF(jniEnv,"我是Native方法sayBye的实现拜拜");
};
/**
 * 声明sayHello和sayBye的类路径
 */
static const char* mClassName="jvm/jni/DynamicDemo";

/**
 * JNINativeMethod结构体
 * 包含:name-方法名;signature-方法签名(描述返回值和入参);fnPtr-c中实现的函数指针
 * 方法签名可以根据javap命令反编译查看或使用jclasslib插件查看
 */
static const JNINativeMethod mMethod[]={
        {"sayHello","()Ljava/lang/String;",fun1},
        {"sayBye","()Ljava/lang/String;",fun2}
};

/**
 *  JNI_OnLoad方法实现
 *  在java中执行System.loadLibrary("dynamicDemoLib");时候加载完dynamicDemoLib动态库后会调用此方法
 * @param vm JNIInvokeInterface_结构体的二级指针(*vm是JNIInvokeInterface_的一级指针因为JavaVM本来就是个一级指针类型)
 * @param reserved
 * @return
 */
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved){
   JNIEnv* env= NULL;
   // 调用GetEnv函数获得JNIEnv指针
    int r=(*vm)->GetEnv(vm,(void**)&env,JNI_VERSION_1_8);
    if(r!=JNI_OK){
        return -1;
    }
    // 调用FindClass函数获取DynamicDemo的class的地址引用
    jclass mClass=(*env)->FindClass(env,mClassName);
    // 注册class与方法与JNINativeMethod的绑定关系,如果小于0则注册失败
    r=(*env)->RegisterNatives(env,mClass,mMethod,2);
    if(r!=JNI_OK){
        return -1;
    }
    return JNI_VERSION_1_8;
};
  • 在CMakeLists.txt添加add_library(dynamicDemoLib SHARED DynamicDemo.c)然后点击buildproject编译工程

在这里插入图片描述

  • 编译完之后在make-build-debug文件夹下找到编译好的动态库文件libdynamicDemoLib.dylib(mac为dylib,windows为dll,linux为so),将其放置在环境变量CLASSPATH的任意路径下都可以,本人放置在%JAVA_HOME%/lib的目录下(对应windows系统的%JAVA_HOME%/jre/bin目录)。
    在这里插入图片描述
    在这里插入图片描述

  • 在java代码中添加静态代码块static{System.loadLibrary(“dynamicDemoLib”);}加载动态库,并运行代码
    在这里插入图片描述

2)原理

动态注册的关键是JNINativeMethod结构体和JNI_OnLoad的实现,JNINativeMethod结构体包含:name-方法名;signature-方法签名(描述返回值和入参);fnPtr-c中实现的函数指针;JNI_OnLoad作用是绑定JNINativeMethod和class直接的关系并返回JNI的版本号。在执行JNI_OnLoad完成注册后当java代码中执行Native方法时根据调用类可以找对应JNINativeMethod再根据方法名和方法签名可以找到对应的C语言函数指针。
优点:
首次执行Native方法时即可根据映射关系直接获取对应的C函数指针,无需全局匹配
对C函数的实现类文件命名没有特殊要求,可以在一个文件里完成多个java文件中多个Native方法的实现和注册

3、jni.h中重要结构体介绍

这里介绍一下我们上面在静态注册和动态注册时用到的C语言的关键结构体

1)JNIEnv *jniEnv

在上面代码中JNIEnv *jniEnv多出现在实现函数的第一个参数,首先看下JNIEnv是什么呢?
在这里插入图片描述

上图是在jni.h里有对应别名声明,JNIEnv等同于JNINativeInterface_结构体即JNIEnv等同&JNINativeInterface_即JNIEnv为JNINativeInterface_结构体的一级指针,所以jniEnv为JNINativeInterface_结构体的一级指针,jniEnv为JNINativeInterface_结构体的二级指针。那JNINativeInterface_结构体又是什么呢?

struct JNINativeInterface_ {
    void *reserved0;
    void *reserved1;
    void *reserved2;

    void *reserved3;
    jint (JNICALL *GetVersion)(JNIEnv *env);

    jclass (JNICALL *DefineClass)
      (JNIEnv *env, const char *name, jobject loader, const jbyte *buf,
       jsize len);
    jclass (JNICALL *FindClass)
      (JNIEnv *env, const char *name);

    jmethodID (JNICALL *FromReflectedMethod)
      (JNIEnv *env, jobject method);
    jfieldID (JNICALL *FromReflectedField)
      (JNIEnv *env, jobject field);

    jobject (JNICALL *ToReflectedMethod)
      (JNIEnv *env, jclass cls, jmethodID methodID, jboolean isStatic);

    jclass (JNICALL *GetSuperclass)
      (JNIEnv *env, jclass sub);
    jboolean (JNICALL *IsAssignableFrom)
      (JNIEnv *env, jclass sub, jclass sup);

    jobject (JNICALL *ToReflectedField)
      (JNIEnv *env, jclass cls, jfieldID fieldID, jboolean isStatic);

    jint (JNICALL *Throw)
      (JNIEnv *env, jthrowable obj);
    jint (JNICALL *ThrowNew)
      (JNIEnv *env, jclass clazz, const char *msg);
    jthrowable (JNICALL *ExceptionOccurred)
      (JNIEnv *env);
    void (JNICALL *ExceptionDescribe)
      (JNIEnv *env);
    void (JNICALL *ExceptionClear)
      (JNIEnv *env);
    void (JNICALL *FatalError)
      (JNIEnv *env, const char *msg);

    jint (JNICALL *PushLocalFrame)
      (JNIEnv *env, jint capacity);
    jobject (JNICALL *PopLocalFrame)
      (JNIEnv *env, jobject result);
 ...省了很多函数

上段代码为JNINativeInterface_在jni.h中的声明,JNINativeInterface_结构体为一个函数表,里面包含了JNI官方提供的常用函数的函数指针,方便我们直接调用。
在这里插入图片描述
上图是JNIEnv*的结构图,JNIEnv是线程唯一的一个线程只有一个JNIEnv,而且不可跨线程调用。

2)JavaVM *vm

在这里插入图片描述

struct JNIInvokeInterface_ {
    void *reserved0;
    void *reserved1;
    void *reserved2;

    jint (JNICALL *DestroyJavaVM)(JavaVM *vm);

    jint (JNICALL *AttachCurrentThread)(JavaVM *vm, void **penv, void *args);

    jint (JNICALL *DetachCurrentThread)(JavaVM *vm);

    jint (JNICALL *GetEnv)(JavaVM *vm, void **penv, jint version);

    jint (JNICALL *AttachCurrentThreadAsDaemon)(JavaVM *vm, void **penv, void *args);
};

同JNIEnv的解释一样直接vm是JNIInvokeInterface_结构体的二级指针,JNIInvokeInterface_也是个函数表包含了五个函数指针,它是虚拟机在JNI层的代表。调用JavaVM的GetEnv函数可以注册JNIEnv;调用JavaVM的AttachCurrentThread函数,就可得到这个线程的JNIEnv结构体;另外,后台线程退出前,需要调用JavaVM的DetachCurrentThread函数来释放对应的资源。

3)JNINativeMethod

在这里插入图片描述

JNINativeMethod是一个结构体包含:name-方法名;signature-方法签名(描述返回值和入参);fnPtr-c中实现的函数指针;作用是用来注册和记录Native方法与C语言实现函数指针直接的关系。

4、基于OpenJDK分析JNI的动态库的加载流程

细心的同学应该发现了我们只是将C语言编译后的动态库放入%JAVA_HOME%/lib(对应windows系统的%JAVA_HOME%/jre/bin目录)的目录下jvm并不会加载此动态库,如果想要加载需要在java显示执行System.loadLibrary(“动态库名”)才行,那么现在就来看下这行代码究竟干了什么。
注:并不是只有我们自己编译的动态库才需要显示执行System.loadLibrary(),JDK自带在%JAVA_HOME%/lib(对应windows系统的%JAVA_HOME%/jre/bin目录)下面的动态库也需要显示执行System.loadLibrary()才会被加载。

以linux平台为例,简单总结一下整个so库的加载流程:

①. 首先 System.loadLibrary() 被调用,开始整个加载过程。

②. 其中调用 ClassLoader 对象来完成主要工作,将每个本地库封装成 NativeLibrary 对象,并以静态变量存到已经加载过的栈中。

③. 执行NativeLibrary 类的 load native方法,来交给native层去指向具体的加载工作。

④. native层 ClassLoader.c 中的 Java_java_lang_ClassLoader_00024NativeLibrary_load 函数被调用。

⑤. 在native load函数中首先使用 dlopen 来加载so本地库文件,并将返回的handle保存到 NativeLibrary对象中。

⑥. 接着查找已经加载的so本地库中的 JNI_OnLoad 函数,并执行它。

⑦. 整个so本地库的加载流程完毕。

当native方法执行时会先尝试按照动态注册执行方式根据调研class查找对于的JNINativeMethod结构体,如果找不到则会按照静态注册执行方式根据方法名与类库中匹配函数,匹配到后会动态维护JNINativeMethod结构体。

基于OpenJDK分析JNI的动态库的加载流程部分参考:https://www.jianshu.com/p/fdf50516faa8

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

躺平程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值