JNI (Java Native Interface) 技术手册中文参考指南

1 引言

本章介绍 Java 本地接口 (JNI)。JNI 是一种原生编程接口,它允许运行在 Java 虚拟机 (VM) 内部的 Java 代码与用其他语言(如 C、C++ 和汇编)编写的应用程序和库进行交互。

JNI 最重要的优势在于:它对底层 Java VM 的实现不施加任何限制。因此,Java VM 供应商可以在不影响 VM 其他部分的前提下添加对 JNI 的支持。程序员可以编写一个版本的原生应用程序或库,并确保它能与所有支持 JNI 的 Java VM 一起工作。

1.1 Java 本地接口概览

虽然您可以完全用 Java 编写应用程序,但某些情况下 Java 本身无法满足您的需求。程序员使用 JNI 编写"Java 本地方法"来处理那些无法完全用 Java 实现的需求。

以下示例说明了何时需要使用 Java 本地方法:

  • 标准 Java 类库不支持应用程序所需的平台相关功能。
  • 您已经拥有用其他语言编写的库,并希望通过 JNI 使 Java 代码能够访问它。
  • 您希望用诸如汇编之类的低级语言实现一小部分时间关键型(time-critical)代码。

通过 JNI 编程,您可以使用本地方法来:

  • 创建、检查和更新 Java 对象(包括数组和字符串)。
  • 调用 Java 方法。
  • 捕获和抛出异常。
  • 加载类并获取类信息。
  • 执行运行时类型检查。

您还可以将 JNI 与"调用接口 (Invocation API)"结合使用,使任意原生应用程序能够嵌入 Java VM。这使得程序员可以轻松地对其现有应用程序进行"Java 化",而无需与 VM 源代码进行链接。

2 设计综述

本章重点探讨 JNI 的主要设计问题,其中大部分内容针对本地方法(Native Methods)。关于调用接口(Invocation API)的设计请见第五章。

2.1 JNI 接口函数与指针

原生代码通过调用 JNI 函数来访问 Java VM 功能。这些函数通过一个接口指针提供。

  • 定义:接口指针是一个指向指针数组的指针,数组中的每个元素都指向一个特定的接口函数。
  • 优势:将 JNI 命名空间与原生代码解耦。VM 可以方便地提供多个版本的函数表。例如,一种版本执行详尽的非法参数检查(适合调试),另一种执行最小化检查以获得高性能。
  • 线程约束:JNI 接口指针仅在当前线程中有效。因此,本地方法严禁将接口指针从一个线程传递到另一个线程。
  • 参数传递:VM 保证在同一个 Java 线程中多次调用本地方法时,传递的接口指针保持一致。但若本地方法被不同线程调用,则可能会接收到不同的指针。

2.2 编译、加载和链接本地方法

  • 编译规范 :由于 Java VM 是多线程的,本地库必须使用支持多线程的环境进行编译(例如在 Linux 上使用 -D_REENTRANT)。
  • 加载方式 :通过 System.loadLibrary 加载。系统会自动处理平台相关的命名规则(如 Unix 下的 libName.so 与 Windows 下的 Name.dll)。
  • 静态链接 :若 OS 不支持动态链接,原生方法必须预先与 VM 链接。此时可使用 RegisterNatives() 函数手动注册本地方法。

2.3. 本地方法名称解析

动态链接器通过名称解析入口。JNI 使用简单的名称重整(Name Mangling)方案以兼容所有 Unicode 字符:

  • 结构Java_ + 类名 + _ + 方法名 + (可选的 __ + 参数签名)。
  • 下划线转义 :为了保证 C 函数名合法,/ 被替换为 __ 被替换为 _1
  • 重载处理:仅在方法重载时才需要使用带有参数签名的长名称。

2.4 本地方法参数

  • 首参数 :永远是 JNIEnv 接口指针。

  • 次参数

    • 若为非静态方法,第二个参数是 jobject(即 this 指针)。
    • 若为静态方法,第二个参数是 jclass(即类引用)。
  • 后续参数:对应 Java 方法的实际参数。

2.5 引用 Java 对象

  • 传递原则:原始类型(int, float 等)直接拷贝;对象类型通过引用传递。

  • 引用分类

    • 局部引用 (Local References) :仅在本地方法调用期间有效,方法返回后由 VM 自动释放。
    • 全局引用 (Global References) :必须通过 NewGlobalRef 显式创建,且必须显式调用 DeleteGlobalRef 释放,否则会造成内存泄漏。
  • 实现机制:VM 为每次从 Java 到本地的控制转移建立一个"注册表 (Registry)",将局部引用映射到对象,确保在 native 调用期间不会被 GC 回收。

2.6 访问 Java 对象

JNI 提供了一套丰富的访问函数。尽管通过这些函数访问对象的性能开销比直接访问 C 数据结构稍高,但对于非平凡的任务(non-trivial tasks),这种开销是可以接受的。

2.7 访问原始数组

为避免频繁函数调用的开销,JNI 提供了一种折中方案:

  • 复制 (Copying) :对于少量元素,使用 Get<Type>ArrayRegion
  • 锁定/固定 (Pinning) :对于大规模运算,使用 GetPrimitiveArrayCritical 请求 VM 锁定内存区域,直接获取指针。使用后必须调用 ReleasePrimitiveArrayCritical 解锁,否则可能导致 GC 挂起或内存碎片化。

2.8 访问字段与方法

JNI 采用两步走策略:

  1. 获取 ID :先通过 GetFieldIDGetMethodID 查找 ID(查找开销大)。
  2. 重复使用:在代码中缓存这些 ID,在后续操作中直接使用(调用开销小)。
  • 注意:如果类被卸载,ID 将失效,必须重新获取。

2.9 编程错误与异常

  • 错误自检 :JNI 为了极致性能,不自动检查非法参数(如 NULL 指针)。开发者有责任确保参数合法

  • 异常处理

    • 本地方法可以抛出 Java 异常。
    • 若 native 代码调用了会导致异常的 JNI 函数,必须在调用其他 JNI 函数前通过 ExceptionOccurredExceptionClear 清理异常。
    • 异步异常:多线程环境下的异步异常只在执行"会抛出同步异常"的 JNI 函数时被感知。

3 数据类型与结构

本章详细说明了 JNI 如何在 Java 类型与原生 C/C++ 类型之间建立映射。

3.1 原始类型

JNI 为原始类型定义了与机器无关的类型别名,以确保代码的跨平台兼容性。

Java 类型 原生类型 说明
boolean jboolean 无符号 8 位 (0: JNI_FALSE, 1: JNI_TRUE)
byte jbyte 有符号 8 位
char jchar 无符号 16 位
short jshort 有符号 16 位
int jint 有符号 32 位
long jlong 有符号 64 位
float jfloat 32 位 (IEEE 754)
double jdouble 64 位 (IEEE 754)
void void 无类型

注:typedef jint jsize; 常用于定义索引和数组长度。

3.2 引用类型

JNI 将 Java 对象映射为引用类型。在 C 中,它们统一映射为 jobject;在 C++ 中,为了支持静态类型检查,定义了继承关系。

层次结构定义:

  • jobject (基类)

    • jclass (类)

    • jstring (字符串)

    • jarray (数组基类)

      • jobjectArray, jbooleanArray, jbyteArray, jcharArray, jshortArray, jintArray, jlongArray, jfloatArray, jdoubleArray
    • jthrowable (异常)

3.3 ID类型

方法 ID (jmethodID) 和字段 ID (jfieldID) 是原生层与 Java 层交互的"索引"。它们在 VM 内部是作为不透明指针 (Opaque Pointers) 存在的,原生层不应直接解引用这些指针。

3.4 值联合体

jvalue 联合体用于在函数参数数组中同时容纳多种 Java 类型,这在动态调用方法时非常有用:

C

ini 复制代码
typedef union jvalue {
    jboolean z; jbyte b; jchar c; jshort s; jint i;
    jlong j; jfloat f; jdouble d; jobject l;
} jvalue;

3.5 类型签名

这是 JNI 与 VM 交互时最核心的部分。Java 方法必须通过签名字符串(Signature)来描述其参数和返回值类型。

类型签名 Java 类型
Z boolean
B byte
C char
S short
I int
J long
F float
D double
Lfully-qualified-class; 全限定类名 (例如: Ljava/lang/String;)
[type 数组 (例如: [I 代表 int[])
(arg-types)ret-type 方法签名 (例如: (I)V 代表 void f(int))

3.6 修改版 UTF-8 字符串

JNI 字符串的处理与标准 UTF-8 有两点不同:

  1. 空字符处理 :标准的 \u0000 在标准 UTF-8 中用单字节表示,但在 JNI 中使用双字节 0xc0 0x80 表示。这意味着JNI 字符串永远不会包含内嵌的 NULL 字符
  2. 增补字符 :对于大于 U+FFFF 的 Unicode 字符(例如某些 Emoji),JNI 使用 UTF-16 代理对(Surrogate Pairs)方式编码,每个代码单元分别编码为 3 字节,共计 6 字节。

4 JNI 函数参考

本章定义了所有 JNI 函数,这些函数通过 JNIEnv 指针访问。开发者必须确保传递给这些函数的参数符合规范(如非 NULL),否则可能导致 VM 崩溃。

4.1 类与对象操作

用于在原生代码中查找类、实例化对象或获取对象类型。

  • 类查询

    • FindClass(env, name):通过全限定名(如 java/lang/String)查找类。
    • GetObjectClass(env, obj):获取指定对象的类。
    • GetSuperclass(env, clazz):获取指定类的父类。
    • IsAssignableFrom(env, clazz1, clazz2):判断类之间的继承/实现关系。
  • 对象创建

    • AllocObject(env, clazz):仅分配内存,不调用构造函数。
    • NewObject(env, clazz, methodID, ...):调用构造函数创建对象。

4.2 字段与方法访问

JNI 通过 ID 来访问成员,这是提升性能的关键。

  • 获取 ID(查找开销高,建议缓存)

    • GetFieldID(env, clazz, name, sig):获取实例字段 ID。
    • GetStaticFieldID(env, clazz, name, sig):获取静态字段 ID。
    • GetMethodID(env, clazz, name, sig):获取实例方法 ID。
    • GetStaticMethodID(env, clazz, name, sig):获取静态方法 ID。
  • 字段读写

    • Get<Type>Field 系列:获取字段值(如 GetObjectField, GetIntField)。
    • Set<Type>Field 系列:设置字段值(如 SetObjectField, SetIntField)。
  • 方法调用

    • Call<Type>Method 系列:调用实例方法(如 CallVoidMethod, CallObjectMethod)。
    • CallStatic<Type>Method 系列:调用静态方法(如 CallStaticVoidMethod)。
    • CallNonvirtual<Type>Method 系列:调用特定父类方法(绕过重写)。

4.3 引用生命周期管理

用于控制 Java 对象的生命周期,防止内存泄漏。

  • 局部引用 (Local References)

    • DeleteLocalRef(env, localRef):显式删除不再需要的引用。
    • PushLocalFrame(env, capacity):创建一个新的局部引用帧。
    • PopLocalFrame(env, result):弹出当前帧并返回一个结果引用。
  • 全局引用 (Global References)

    • NewGlobalRef(env, obj):创建跨方法、跨线程的全局引用。
    • DeleteGlobalRef(env, globalRef):显式释放全局引用。
  • 弱全局引用 (Weak Global References)

    • NewWeakGlobalRef(env, obj):创建不阻碍 GC 的弱引用。
    • DeleteWeakGlobalRef(env, obj):删除弱引用。

4.4 异常处理

用于捕获和处理 Java 层抛出的异常。

  • Throw(env, throwable):抛出一个已存在的异常对象。
  • ThrowNew(env, clazz, msg):构造并抛出一个新异常。
  • ExceptionOccurred(env):返回当前挂起的异常对象。
  • ExceptionCheck(env):高效检查是否有异常挂起(不返回异常对象,仅返回布尔值)。
  • ExceptionClear(env):清除当前异常,使代码恢复正常状态。
  • ExceptionDescribe(env):打印异常堆栈信息到 stderr。

4.5 数组操作

提供高效的数组读写接口。

  • GetArrayLength(env, array):获取数组大小。

  • 元素访问

    • Get<Type>ArrayElements:获取数组元素指针(可能产生拷贝)。
    • Release<Type>ArrayElements:释放访问权限。
    • Get<Type>ArrayRegion / Set<Type>ArrayRegion:直接读写数组的指定片段(最高效,适用于小范围访问)。
  • 关键锁访问

    • GetPrimitiveArrayCritical / ReleasePrimitiveArrayCritical:获取直接指针,在代码关键区(Critical Section)中使用。

4.6 字符串操作

  • NewString(env, unicodeChars, len):通过 Unicode 字符创建字符串。
  • NewStringUTF(env, bytes):通过修改版 UTF-8 编码创建字符串。
  • GetStringLength(env, string):获取字符串长度。
  • GetStringUTFChars(env, string, isCopy):获取 UTF-8 字符指针。
  • ReleaseStringUTFChars(env, string, chars):释放字符指针内存。

其它函数查看入口

5 调用接口

调用接口 (Invocation API) 允许软件供应商将 Java 虚拟机 (VM) 加载到任意原生应用程序中。这使得供应商可以在无需与 Java VM 源代码进行静态链接的情况下,开发并交付支持 Java 的应用程序。

5.1 概述

调用接口允许原生应用程序通过 JNI 接口指针访问 VM 的功能。其设计理念与 Netscape 的 JRI 嵌入接口类似。

典型应用流程:

  1. 创建并初始化 Java VM。
  2. 通过 JNIEnv 接口指针执行 Java 方法(如 Main.test)。
  3. 销毁 VM。

5.2 核心 API 函数

虽然 JavaVM 结构体包含了一个接口函数表,但以下三个关键函数是"独立"的(即在创建 VM 之前即可调用,不属于 JavaVM 结构体内部):

  • JNI_CreateJavaVM :加载并初始化 Java VM。调用此函数的线程成为"主线程"。它会将 env 参数设置为该线程的 JNI 接口指针。
  • JNI_GetCreatedJavaVMs:获取当前进程中已创建的所有 Java VM。
  • JNI_GetDefaultJavaVMInitArgs:获取 VM 的默认配置参数。

5.3 JavaVM 接口函数表

一旦 VM 被创建,原生代码即可通过 JavaVM 指针访问以下函数:

函数名 功能描述
DestroyJavaVM 卸载 Java VM 并回收资源。
AttachCurrentThread 将当前原生线程挂载到 VM,并获取该线程的 JNIEnv
DetachCurrentThread 将当前线程从 VM 中分离。
GetEnv 获取当前线程已有的 JNIEnv 指针。
AttachCurrentThreadAsDaemon 挂载线程并将其标记为 Daemon(守护线程)。

5.4 关键概念详解

A. 线程挂载
  • 重要性 :JNI 接口指针 (JNIEnv) 是线程私有的。如果一个原生线程未挂载到 VM 而直接使用 JNIEnv,会导致不可预知的错误。
  • 挂载方式 :通过 AttachCurrentThread() 将当前线程挂载。挂载后,该线程的行为就像运行在 Java 本地方法中的常规 Java 线程。
  • 分离机制 :在线程退出前,必须 调用 DetachCurrentThread()。若栈上仍存在 Java 方法调用,则无法分离。
B. 虚拟机卸载
  • 限制:VM 会等到"当前线程成为唯一的非守护用户线程"时,才会真正执行卸载。这是因为 VM 无法自动释放由原生代码持有的系统资源(如锁、文件句柄等),此责任需由程序员承担。
C. 初始化参数

这是调用接口的配置核心,包含以下参数:

  • version:必须指定 JNI 版本(如 JNI_VERSION_1_6)。

  • nOptions:选项数量。

  • options:一个 JavaVMOption 数组,用于设置:

    • -Djava.class.path:指定 Java 类路径。
    • -verbose:jni:打印 JNI 相关调试信息。
    • -Xms / -Xmx:设置初始及最大堆大小。
  • ignoreUnrecognized:若为 false,遇到不认识的选项(如 -X 开头的非标参数)会直接报错。

5.5 库与版本管理

为了支持更复杂的动态类加载(JDK 1.2+),JNI 引入了库生命周期钩子:

  • JNI_OnLoad(JavaVM *vm, void *reserved)

    • 当原生库被 System.loadLibrary 加载时,VM 会自动调用此函数。
    • 必须 返回当前库所需的 JNI 版本号(例如 JNI_VERSION_1_4)。若版本不匹配,库将加载失败。
  • JNI_OnUnload(JavaVM *vm, void *reserved)

    • 当包含该库的类加载器被 GC 回收时调用。常用于执行必要的清理操作(如注销原生回调、关闭句柄)。

如果在此文章中您有所收获,请给作者一个鼓励,点个赞,谢谢支持

技术变化都很快,但基础技术、理论知识永远都是那些;作者希望在余后的生活中,对常用技术点进行基础知识分享;如果你觉得文章写的不错,请给予关注和点赞;如果文章存在错误,也请多多指教!

相关推荐
东坡白菜2 小时前
破局全栈:前端开发的Java入门实战记录—JPA(2)
java·后端
Coffeeee9 小时前
如何使用Glide和Coil加载WebP动图
android·kotlin·glide
SimonKing9 小时前
艹,维护AI写的代码,我心态崩了......
java·后端·程序员
用户298698530149 小时前
Java Word 文档样式进阶:段落与文本背景色设置完全指南
java·后端
Kapaseker9 小时前
5 分钟搞懂 Kotlin DSL
android·kotlin
恋猫de小郭10 小时前
AI Agent 开发究竟是啥?如何用 AI 开发 Agent ?深入浅出给你一套概念
android·前端·ai编程
黄林晴10 小时前
Android 17 正式发布!target 37 一大批旧代码直接不能用了
android
Carson带你学Android10 小时前
Android 17 正式发布:AI 终于成了系统能力
android·前端·ai编程
三少爷的鞋11 小时前
当 UseCase 开始长期监听,它可能已经不是 UseCase 了
android