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

JNI

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

使用JNI能让Java与本地语言交互,但通常也意味着丧失了跨平台性,而有些场合会使用。好比标准的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方法来加载,加载的大体逻辑为:linux

  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,它会根据不一样的操做系统作不一样的处理。windows

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三大类型操做系统的处理,下面分别看看不一样操做系统如何处理。安全

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

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

#define JNI_LIB_SUFFIX ".so"

#define JNI_LIB_SUFFIX ".dll"复制代码

字节码

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

public static native byte[] encrypt(byte[]);
    descriptor: ([B)[B
    flags: ACC_PUBLIC, ACC_STATIC, ACC_NATIVE复制代码

总结一下

两句话总结起来就是,Java编译器将包含本地方法的class对应的方法添加ACC_NATIVE标识,而JVM负责将动态库加载到内存,Java执行引擎执行到本地方法时找到对应的函数,完成本地方法的调用。jvm

如下是广告相关阅读函数

========广告时间========性能

鄙人的新书《Tomcat内核设计剖析》已经在京东销售了,有须要的朋友能够到 item.jd.com/12185360.ht… 进行预约。感谢各位朋友。

为何写《Tomcat内核设计剖析》

=========================

相关阅读:
注解的原理又是怎么一回事

欢迎关注:

相关文章
相关标签/搜索