2021-05-04 19:36  阅读(2599)
文章分类:从 JDK 源码看 Java 文章标签:JavaJDK 源码
©  原文作者:超人汪小建(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三大类型操作系统的处理,下面分别看看不同操作系统如何处理。

202105041936296381.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
    
点赞(2)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> Java调用本地方法又是怎么一回事
上一篇
从JDK源码角度看java并发线程的中断