当前位置: 首页 > news >正文

JVM——JNI 的运行机制

引入

在 Java 开发中,我们常常会遇到一些 Java 语言难以直接处理的场景,例如需要调用特定体系架构或操作系统的功能,或者利用汇编语言的 SIMD 指令来优化关键代码性能。这时,Java Native Interface(JNI)就成为了我们实现跨语言调用的强大工具。JNI 允许我们在 Java 代码中调用 C/C++ 代码,以实现所需功能。

JNI 的基本概念

JNI 是 Java 虚拟机提供的一种机制,用于在 Java 代码中调用原生代码(通常为 C/C++ 代码)。它使得 Java 应用程序能够与操作系统的本地函数库交互,从而实现对硬件资源的直接访问、调用系统 API 以及执行性能敏感型任务等功能。

JNI 的主要特点包括:

  • 平台相关性 :JNI 允许 Java 应用调用特定平台的本地代码,因此使用 JNI 开发的应用通常不具备 Java 的跨平台特性,只能在特定平台上运行。

  • 性能优化 :对于一些对性能要求极高的计算任务,通过 JNI 调用原生代码可以绕过 Java 虚拟机的一些性能限制,直接利用底层资源,从而获得更高的执行效率。

  • 功能扩展 :JNI 为 Java 应用提供了访问原生库中丰富功能的途径,这些功能可能是 Java 标准类库所不具备的。

native 方法的链接

在 Java 中,native 方法是指那些没有提供 Java 实现,而是通过 JNI 调用原生代码的方法。当在 Java 代码中调用这些 native 方法时,Java 虚拟机需要将它们链接到对应的原生函数上。native 方法的链接主要有两种方式:

自动链接

Java 虚拟机会自动查找符合默认命名规范的原生函数,并将 native 方法链接到这些函数上。为了生成符合命名规范的原生函数声明,我们可以使用 javac -h 命令。例如,对于以下 Java 类:

package org.example;
​
public class Foo {public static native void foo();public native void bar(int i, long j);public native void bar(String s, Object o);
}

执行 javac -h . org/example/Foo.java 命令后,会在当前目录下生成一个名为 org_example_Foo.h 的头文件。该头文件中包含了符合 JNI 命名规范的原生函数声明。命名规范要求原生函数以 Java_ 为前缀,接着是完整的包名和类名,中间用下划线连接,并且将原类名中的斜杠 / 替换为下划线 _。如果存在重载方法,则还需要将方法的参数类型编码追加到函数名后,参数类型中的特殊字符(如 ;[)也会被替换为 _2_3 等。

显式链接

除了自动链接方式外,还可以在原生代码中显式地进行 native 方法的链接。这种方式通常会使用一个名为 registerNatives 的 native 方法,并在其中调用 RegisterNatives JNI API 来手动注册其他 native 方法与原生函数的对应关系。例如,Java 中的 Object 类就使用了这种方式来注册其 native 方法。这种方式使得原生函数的命名可以更加灵活,不受默认命名规范的限制。

static JNINativeMethod methods[] = {{"hashCode", "()I", (void *)&JVM_IHashCode},{"wait", "(J)V", (void *)&JVM_MonitorWait},{"notify", "()V", (void *)&JVM_MonitorNotify},{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};
​
JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls) {(*env)->RegisterNatives(env, cls, methods, sizeof(methods) / sizeof(methods[0]));
}

在使用显式链接时,需要注意必须在原生函数被调用之前完成注册工作。通常会在 Java 类的静态初始化块中调用 registerNatives 方法来完成注册。

JNI 的 API

JNI 提供了一系列丰富的 API,允许原生代码访问和操作 Java 虚拟机中的各种资源。这些 API 通过一个名为 JNIEnv 的数据结构提供给原生代码。JNIEnv 是一个线程私有的数据结构,每个 Java 线程都有一个对应的 JNIEnv 实例。它包含了指向各种 JNI 函数的指针,原生代码可以通过这些指针调用 JNI 函数。

数据类型映射

JNI 定义了一套与 Java 基本类型相对应的 C 语言数据类型,例如 jint 对应 Java 的 int 类型,jlong 对应 Java 的 long 类型等。对于引用类型,JNI 提供了 jobject 类型及其派生类型(如 jclassjstringjarray 等)来表示 Java 中的各种对象引用。

常见 API 函数

对象操作 :可以创建 Java 对象、获取对象的类信息、访问对象的字段和方法等。例如,NewObject 用于创建新对象,GetObjectClass 用于获取对象的类,GetFieldIDGetObjectField 用于访问对象的字段。

数组操作 :提供了操作 Java 数组的 API,如 NewIntArray 创建新的 int 数组,GetIntArrayElements 获取数组元素等。

字符串操作 :可以将 Java 的 String 对象转换为 C 风格的字符串,以及反之。例如,GetStringUTFChars 用于获取 String 对象的 UTF-8 编码的 C 字符串,NewStringUTF 用于从 C 字符串创建 Java String 对象。

异常处理 :JNI 提供了处理异常的 API,如 ExceptionOccurred 用于检查是否发生了异常,ExceptionDescribe 用于输出异常信息,ExceptionClear 用于清除已发生的异常等。

局部引用与全局引用

在原生代码中,引用 Java 对象时需要考虑垃圾回收的问题。JNI 提供了局部引用和全局引用来解决这一问题。

局部引用

局部引用是在原生函数调用期间有效的引用。它们在原生函数返回之前一直有效,一旦原生函数返回,局部引用将自动被释放。局部引用主要用于确保在原生代码执行期间,所引用的 Java 对象不会被垃圾回收。例如,原生函数的参数、JNI API 创建的对象等都是局部引用。

JNIEXPORT void JNICALL Java_org_example_Foo_nativeMethod(JNIEnv *env, jobject obj) {jclass cls = (*env)->GetObjectClass(env, obj);jfieldID fid = (*env)->GetFieldID(env, cls, "instanceVar", "I");jint value = (*env)->GetIntField(env, obj, fid);printf("Value: %d\n", value);// 这里 cls、fid、value 都是局部引用,原生函数返回后它们将被自动释放
}

全局引用

全局引用与局部引用不同,它在整个应用程序的生命周期内有效,直到显式地被释放。如果需要在多个原生函数调用之间共享对某个 Java 对象的引用,或者需要缓存某些对象以供后续使用,则需要使用全局引用。全局引用可以通过 NewGlobalRef 函数从局部引用水创建,使用完毕后需要通过 DeleteGlobalRef 显式释放。

jobject globalRef;
​
JNIEXPORT void JNICALL Java_org_example_Foo_init(JNIEnv *env, jobject obj) {globalRef = (*env)->NewGlobalRef(env, obj);// 现在 globalRef 是一个全局引用,可以安全地在其他原生函数中使用
}
​
JNIEXPORT void JNICALL Java_org_example_Foo_cleanup(JNIEnv *env, jobject obj) {(*env)->DeleteGlobalRef(env, globalRef);// 释放全局引用,允许垃圾回收
}

引用管理的注意事项

避免引用泄漏 :由于全局引用会阻止 Java 对象被垃圾回收,因此必须确保在不再需要时及时释放全局引用。否则,可能会导致内存泄漏。

局部引用的限制 :局部引用只能在原生函数调用期间使用。如果需要将引用传递给其他线程或者保存到后续调用中,必须使用全局引用。

性能开销 :频繁地创建和销毁全局引用可能会增加垃圾回收的负担,进而影响应用程序的性能。因此,在使用全局引用水时需要权衡其必要性和性能影响。

JNI 的调用过程与性能开销

调用过程

当 Java 代码调用 native 方法时,Java 虚拟机会执行以下步骤:

  1. 查找原生函数 :根据 native 方法的链接方式(自动链接或显式链接),找到对应的原生函数。

  2. 准备调用 :将 Java 虚拟机的执行环境切换到原生代码执行模式,包括设置原生函数的参数、保存 Java 虚拟机的上下文等。

  3. 执行原生函数 :调用原生函数,原生函数使用 JNI API 与 Java 虚拟机进行交互,访问 Java 对象、调用 Java 方法等。

  4. 返回 Java 环境 :原生函数执行完毕后,将控制权返回给 Java 虚拟机,恢复 Java 执行环境。

性能开销

JNI 调用会引入一定的性能开销,主要包括以下几个方面:

  • 上下文切换 :在 Java 和原生代码之间切换执行环境会产生一定的开销,包括保存和恢复寄存器、堆栈指针等。

  • 数据类型转换 :Java 和 C/C++ 的数据类型表示不同,JNI 需要在两者之间进行数据类型转换,这也会占用一定的时间。

  • 句柄管理 :为了支持垃圾回收,JNI 使用句柄来间接引用 Java 对象。句柄的管理和使用会增加内存访问的开销。

  • 跨语言调用开销 :跨语言调用涉及到的调用约定、参数传递等也会带来额外的性能损耗。

JNI 的异常处理

在 JNI 编程中,异常处理是一个重要的方面。JNI 提供了处理异常的 API,允许原生代码检测、抛出和清除异常。

检测异常

在调用可能抛出异常的 JNI 函数后,可以通过 ExceptionOccurred 函数检测是否发生了异常。如果发生了异常,可以使用 ExceptionDescribe 输出异常信息。

JNIEXPORT void JNICALL Java_org_example_Foo_nativeMethod(JNIEnv *env, jobject obj) {jclass cls = (*env)->FindClass(env, "org/example/SomeClass");if ((*env)->ExceptionOccurred(env)) {(*env)->ExceptionDescribe(env);(*env)->ExceptionClear(env);return;}// 继续处理...
}

抛出异常

原生代码可以使用 ThrowThrowNew 函数抛出异常。Throw 函数用于抛出已有的异常对象,而 ThrowNew 函数用于创建并抛出新的异常对象。

JNIEXPORT void JNICALL Java_org_example_Foo_nativeMethod(JNIEnv *env, jobject obj) {jclass exceptionCls = (*env)->FindClass(env, "java/lang/Exception");(*env)->ThrowNew(env, exceptionCls, "An error occurred in native code");
}

清除异常

在某些情况下,原生代码可能需要清除已发生的异常,以便继续执行或重新抛出新的异常。可以使用 ExceptionClear 函数来清除异常。

if ((*env)->ExceptionOccurred(env)) {(*env)->ExceptionClear(env);// 处理异常或重新抛出...
}

实际应用案例

性能优化案例

假设我们有一个图像处理应用程序,其中包含一个对性能要求极高的滤镜算法。该算法在 Java 中实现时性能不够理想,因此我们决定使用 JNI 调用 C 语言实现的版本。通过 JNI,我们将算法的核心部分移植到 C 语言中,并利用其对底层硬件的直接访问能力,实现了显著的性能提升。

系统集成案例

在另一个场景中,我们需要开发一个与特定硬件设备交互的 Java 应用程序。该硬件设备提供了 C 语言的 SDK,我们通过 JNI 在 Java 应用中调用这些 C 函数,实现了对硬件设备的控制和数据采集。这使得 Java 应用能够充分利用硬件设备的功能,满足了项目需求。

JNI 的优点与局限性

优点

功能扩展性强 :能够调用原生代码,访问 Java 核心类库无法提供的功能。

性能优化潜力大 :对于性能敏感型任务,原生代码通常能提供更高的执行效率。

兼容性好 :可以与现有的 C/C++ 库进行集成,充分利用已有的代码资源。

局限性

破坏可移植性 :使用了 JNI 的 Java 应用通常依赖于特定平台的原生库,导致应用无法跨平台运行。

开发复杂度高 :JNI 涉及 Java 和 C/C++ 两种语言的混合编程,增加了开发和调试的难度。

安全风险高 :原生代码中的错误(如内存泄漏、野指针等)可能会导致 Java 虚拟机崩溃或出现不稳定的行为。

总结

本文详细介绍了 JNI 的运行机制,包括 native 方法的链接、JNI 的 API、局部引用与全局引用等内容。通过学习 JNI 的相关知识,我们能够更好地理解 Java 与原生代码之间的交互方式,掌握在 Java 应用中调用原生代码的方法和技巧,从而在实际开发中充分利用 JNI 的优势,解决 Java 语言难以直接处理的问题。

最佳实践

  1. 尽量减少 JNI 的使用 :由于 JNI 会破坏 Java 应用的可移植性,并且增加了开发和维护的复杂度,因此在可能的情况下,应优先考虑使用纯 Java 实现。

  2. 封装原生代码 :将原生代码封装在一个单独的模块中,与 Java 代码的其他部分解耦,这样可以降低维护难度,并且便于后续的修改和替换。

  3. 严格管理引用 :合理使用局部引用和全局引用,避免出现内存泄漏等问题。在不再需要全局引用时,及时释放它。

  4. 充分测试 :JNI 代码的调试和测试相对复杂,需要在不同的平台和环境下进行充分的测试,以确保原生代码的正确性和稳定性。

相关文章:

  • 【Linux】进程问题--僵尸进程
  • 神经网络加上注意力机制,精度反而下降,为什么会这样呢?注意力机制的本质是什么?如何正确使用注意力机制?注意力机制 | 深度学习
  • xml双引号可以不转义
  • 购物车系统的模块化设计:从加载到结算的全流程拆解
  • SpringBoot返回xml
  • HttpServletRequest 对象包含了哪些信息?
  • 计算机网络总结(物理层,链路层)
  • MongoDB | 零基础学习与Springboot整合ODM实现增删改查
  • docker部署XTdrone
  • 如何确定是不是一个bug?
  • HDFS存储原理与MapReduce计算模型
  • 0基础 Git 代码操作
  • Python实例题:Python打造漏洞扫描器
  • 【Linux 学习计划】-- 冯诺依曼体系 | 操作系统的概念与定位,以及其如何管理软件
  • svn: E155017: Checksum mismatch while updating 校验错误的解决方法
  • whisper相关的开源项目 (asr)
  • leetcode 17. Letter Combinations of a Phone Number
  • Ubuntu 24.04部署安装Honeyd蜜罐
  • 大学之大:浦项科技大学2025.5.25
  • 塔能科技:以多元技术赋能全行业能耗节能转型
  • 网站制作的评价标准/seo搜索排名优化方法
  • 国外门户网站设计/腾讯企业qq官网
  • 做网站需要提供什么资料/短视频推广平台
  • 江津网站建设方案/青岛网站优化公司
  • 做网站都有那些步骤/站长查询
  • 云主机 网站吗/全球网站流量查询