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

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

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

这里写图片描述

  • 对于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

总结一下

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

以下是广告相关阅读

========广告时间========

鄙人的新书《Tomcat内核设计剖析》已经在京东销售了,有需要的朋友可以到 https://item.jd.com/12185360.html 进行预定。感谢各位朋友。

为什么写《Tomcat内核设计剖析》

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

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

欢迎关注:

这里写图片描述

目录
相关文章
|
6天前
|
Java
Java中的内置锁synchronized关键字和wait()、notifyAll()方法
【6月更文挑战第17天】Java的synchronized和wait/notify实现顺序打印ALI:共享volatile变量`count`,三个线程分别检查`count`值,匹配时打印并减1,未匹配时等待。每个`print`方法加锁,确保互斥访问。代码示例展示了线程同步机制。考虑异常处理及实际场景的扩展需求。
39 3
|
2天前
|
IDE Java 编译器
五分钟学Java:可变参数究竟是怎么一回事?
五分钟学Java:可变参数究竟是怎么一回事?
|
10天前
|
Java 调度
Java并发基础-线程简介(状态、常用方法)
Java并发基础-线程简介(状态、常用方法)
15 0
|
3天前
|
NoSQL Java Redis
如何在 Java 中操作这些 Redis 数据结构的基本方法
如何在 Java 中操作这些 Redis 数据结构的基本方法
9 2
|
7天前
|
Arthas 存储 监控
性能监控之常见 Java Heap Dump 方法
【6月更文挑战8天】性能监控之常见 Java Heap Dump 方法
29 7
|
3天前
|
安全 Java 程序员
【程序猿逆袭指南】Java高手的秘密武器:throws关键字,让你的方法签名霸气侧漏!
【6月更文挑战第19天】`throws`关键字是Java异常处理的关键,用于方法签名中声明可能抛出的异常,提示调用者处理。它增进代码可读性和安全性,避免运行时崩溃。通过`throws`声明多个异常,能精细规划错误处理。掌握其使用,能提升代码质量和程序员的专业形象,是Java编程中的必备技能。
|
3天前
|
安全 Java 开发者
【技术咖必看】Java异常处理新境界:throws关键字,打造万无一失的方法签名!
【6月更文挑战第19天】在Java异常处理中,`throws`关键字用于方法签名,声明可能抛出的异常,提示调用者必须处理。它区分运行时异常和检查型异常,常用于声明需要调用者捕获的检查型异常。例如,`readFile`方法`throws IOException`,强制调用者通过try-catch或同样`throws`。多异常声明允许一次声明多个可能的异常类型,增强代码健壮性。理解并善用`throws`能构建更稳定、可读的代码。
|
5天前
|
Java
在 Java 中,类是一种定义对象的模板,它包含数据成员(字段)和方法。
在 Java 中,类是一种定义对象的模板,它包含数据成员(字段)和方法。
|
8天前
|
存储 安全 Java
深入理解Java中的ThreadLocal机制:原理、方法与使用场景解析
深入理解Java中的ThreadLocal机制:原理、方法与使用场景解析
18 2
|
12天前
|
存储 缓存 Java
Java遍历Map集合的方法
在Java中,遍历Map集合主要有四种方式:1) 使用`keySet()`遍历keys并用`get()`获取values;2) 使用`entrySet()`直接遍历键值对,效率较高;3) 通过`Iterator`遍历,适合在遍历中删除元素;4) Java 8及以上版本可用`forEach`和Lambda表达式,简洁易读。`entrySet()`通常性能最佳,而遍历方式的选择应考虑代码可读性和数据量。
28 0