Java调用本地方法又是怎么一回事

撸了今年阿里、腾讯和美团的面试,我有一个重要发现…….

作者:超人汪小建(seaboat)

出处:https://blog.csdn.net/wangyangzhizhou/column/info/16032


JNI

JNI即Java Native Interface,它能在Java层实现对本地方法的调用,一般本地的实现语言主要是C/C++,其实从虚拟机层面来看JNI挺好理解,JVM主要使用C/C++ 和少量汇编编写,在执行Java字节码时如果遇到有某个方法标明为Native的则从JVM中找到对应的C/C++函数,一般本地方法对应的函数会被注册到JVM中。

使用JNI能让Java与本地语言交互,但一般也意味着丧失了跨平台性,而有些场合会使用。比如标准的Java特性不符合你的需求时,比如在性能要求很高的某段逻辑。

从一个例子说起

  • 编写一个Java类提供本地加密的方法,其中加密方法为本地方法,实现是在ByteCodeEncryptor动态库。
    package com.seaboat.bytecode;

    public class ByteCodeEncryptor {
      static{
        System.loadLibrary("ByteCodeEncryptor");
      }

      public native static byte[] encrypt(byte[] text);

    }
  • 为方便起见,不自己写头文件,用javah -jni com.seaboat.bytecode.ByteCodeEncryptor生成。
    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class com_seaboat_bytecode_ByteCodeEncryptor */

    #ifndef _Included_com_seaboat_bytecode_ByteCodeEncryptor
    #define _Included_com_seaboat_bytecode_ByteCodeEncryptor
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     com_seaboat_bytecode_ByteCodeEncryptor
     * Method:    encrypt
     * Signature: ([B)[B
     */
    JNIEXPORT jbyteArray JNICALL Java_com_seaboat_bytecode_ByteCodeEncryptor_encrypt
      (JNIEnv *, jclass, jbyteArray);

    #ifdef __cplusplus
    }
    #endif
    #endif

  • 编写源文件,实现头文件声明的函数。
    #include "com_seaboat_bytecode_ByteCodeEncryptor.h"
    #include "jni.h"

    void encode(char *str)
    {
        unsigned int m = strlen(str);
        for (int i = 0; i < m; i++)
        {
            str[i] = str[i]+4;
        }

    }

    extern"C" JNIEXPORT jbyteArray JNICALL
    Java_com_seaboat_bytecode_ByteCodeEncryptor_encrypt(JNIEnv * env, jclass cla,jbyteArray text)
    {
        char* dst = (char*)env->GetByteArrayElements(text, 0);
        encode(dst);
        env->SetByteArrayRegion(text, 0, strlen(dst), (jbyte *)dst);
        return text;
    }
  • 用cl进行编译,生成动态库,指定编译需要的一些头文件。
    cl /EHsc -ID:\Java\jdk1.8.0_73\include\ -ID:\Java\jdk1.8.0_73\include\win32 -LD com_seaboat_bytecode_ByteCodeEncryptor.cpp -FeByteCodeEncryptor.dll
  • 可以调用Java层的ByteCodeEncryptor类的encrypt方法了。

怎么加载动态库

Java层需要调用System.loadLibrary去加载动态库,而它其实就是通过ClassLoaderloadLibrary方法来加载,加载的大致逻辑为:

  1. 是不是使用了绝对路径来指定动态库,如果是则直接通过绝对路径来加载。
  2. 如果启动Java时带有-Dsun.boot.library.path=xxxx时,则去改参数指定的目录下寻找动态库。
  3. 如果启动Java时带有-Djava.library.path=xxxx时,则去改参数指定的目录下寻找动态库。
    加载动态库在Java层面实现不了,所以必须会通过本地才能真正实现加载操作,Java层面最后是走到NativeLibrary类,其包含的load本地方法为真正的加载注册操作。

对应着ClassLoader.cJava_java_lang_ClassLoader_00024NativeLibrary_load函数,因为NativeLibrary在Java层的ClassLoader的子类,所以其中包含一串数字00024,即表示美元符号。该函数最重要的一步是调了JVM_LoadLibrary函数,该函数如下,核心的一步是os::dll_load,它会根据不同的操作系统做不同的处理。

    JVM_ENTRY_NO_ENV(void*, JVM_LoadLibrary(const char* name))
      //%note jvm_ct
      JVMWrapper2("JVM_LoadLibrary (%s)", name);
      char ebuf[1024];
      void *load_result;
      {
        ThreadToNativeFromVM ttnfvm(thread);
        load_result = os::dll_load(name, ebuf, sizeof ebuf);
      }
      if (load_result == NULL) {
        char msg[1024];
        jio_snprintf(msg, sizeof msg, "%s: %s", name, ebuf);
        // Since 'ebuf' may contain a string encoded using
        // platform encoding scheme, we need to pass
        // Exceptions::unsafe_to_utf8 to the new_exception method
        // as the last argument. See bug 6367357.
        Handle h_exception =
          Exceptions::new_exception(thread,
                                    vmSymbols::java_lang_UnsatisfiedLinkError(),
                                    msg, Exceptions::unsafe_to_utf8);

        THROW_HANDLE_0(h_exception);
      }
      return load_result;
    JVM_END

看一个图,它包含了linuxsolariswindows三大类型操作系统的处理,下面分别看看不同操作系统如何处理。

20190705310_1.png

  • 对于linux,主要通过dlopen函数来打开动态库,并加载到内存中,再通过dlsym函数可以获取动态库中的函数指针,于是就能实现调用动态库某函数。
  • 对于solaris,主要通过dlopen函数来打开动态库,并加载到内存中,再通过dlsym函数可以获取动态库中的函数指针,但它与linux不同的是dlsym在linux中是非线程安全的,需要加锁,而solaris则不需要。
  • 对于windows,主要通过LoadLibrary函数加载动态库,加载到内存中,再通过GetProcAddress函数可以获取动态库的函数指针,从而实现调用动态库某函数。

另外,我们注意到Java层不必指定动态库的后缀,这个留给JVM去解决,它会根据不同操作系统添加不同的后缀,这个逻辑由System.cJava_java_lang_System_mapLibraryName函数实现,它会有如下两个后缀。

    #define JNI_LIB_SUFFIX ".so"

    #define JNI_LIB_SUFFIX ".dll"

字节码

对于字节码,它是Java执行时的指令,其实想一下就能想到本地方法要在执行时区别于Java层的调用,所以必须要有一个flag来标识本地方法,那咱们用javap来看看上面包含本地方法的class会有什么标识,可以看到存在一个ACC_NATIVE,有了它就可以在执行时调用C/C++函数了。

    public static native byte[] encrypt(byte[]);
        descriptor: ([B)[B
        flags: ACC_PUBLIC, ACC_STATIC, ACC_NATIVE
赞(1) 打赏

如未加特殊说明,此网站文章均为原创,转载必须注明出处。Java 技术驿站 » Java调用本地方法又是怎么一回事
分享到: 更多 (0)

评论 抢沙发

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

关注【Java 技术驿站】公众号,每天早上 8:10 为你推送一篇技术文章

扫描二维码关注我!


关注【Java 技术驿站】公众号 回复 “VIP”,获取 VIP 地址永久关闭弹出窗口

免费获取资源

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏