on
JNI
前提
关于 JNI 的文章很多,但大多都是写怎么使用,本文主要讲述两件事:
- 数据类型到底是怎么在 jvm 与 native 之间互传的
- jvm 与 native 的函数调用具体是如何执行的
OpenJDK
扯到 JNI,绕不过 jdk(源码就是最好的文档),而开源版本中使用最广泛的就是 OpenJDK。
- 官网地址 - openjdk
- jdk 项目
- github jdk
- jdk-23-ga (本文源码均引用 jdk 23 正式发布版本)
使用
JNI 参数类型
JNI支持多种Java类型和其对应的原生(Native)类型,以便在Java和C/C++之间传递数据。以下是JNI支持的参数类型,以及它们的映射关系。
1. 基本数据类型
JNI直接支持的基本数据类型:
Java类型 | JNI类型 | C/C++ 类型 |
---|---|---|
boolean |
jboolean |
unsigned char (0或1) |
byte |
jbyte |
signed char |
char |
jchar |
unsigned short |
short |
jshort |
short |
int |
jint |
int |
long |
jlong |
long long |
float |
jfloat |
float |
double |
jdouble |
double |
void |
void |
void |
这些类型是Java和C之间的基本数据类型对应关系,可以直接用于参数传递和返回。
2. 对象类型
JNI支持Java对象类型传递给C/C++代码。常见的对象类型及其对应关系如下:
Java类型 | JNI类型 | 描述 |
---|---|---|
Object |
jobject |
Java的任意对象 |
String |
jstring |
Java字符串 |
Class |
jclass |
Java类 |
Throwable |
jthrowable |
异常类 |
Boolean |
jobject |
Java包装类型 |
Integer |
jobject |
Java包装类型 |
Double |
jobject |
Java包装类型 |
数组类型 | 各种j*Array |
包括jbooleanArray 等 |
3. 数组类型
JNI支持Java数组,可以传递给C/C++。不同的基本数据类型数组都有对应的JNI类型:
Java数组类型 | JNI类型 | 描述 |
---|---|---|
boolean[] |
jbooleanArray |
布尔数组 |
byte[] |
jbyteArray |
字节数组 |
char[] |
jcharArray |
字符数组 |
short[] |
jshortArray |
短整型数组 |
int[] |
jintArray |
整型数组 |
long[] |
jlongArray |
长整型数组 |
float[] |
jfloatArray |
浮点型数组 |
double[] |
jdoubleArray |
双精度浮点型数组 |
Object[] |
jobjectArray |
对象数组 |
数组操作的常用函数
在JNI中,可以通过一些函数操作数组,如GetArrayLength
、GetIntArrayElements
、SetIntArrayRegion
等来读写Java数组。
4. 特殊类型
JNI还定义了一些特殊类型来帮助跨语言调用:
JNI 类型 | 描述 |
---|---|
JNIEnv* |
JNI环境指针,每个线程都有独立的JNIEnv |
JavaVM* |
Java虚拟机指针,用于全局获取Java虚拟机实例 |
jfieldID |
Java类字段的引用ID |
jmethodID |
Java类方法的引用ID |
jvalue |
JNI函数调用中使用的参数联合类型 |
示例代码
下面是一个带有数组和字符串参数的JNI方法示例:
public class ExampleJNI {
// 声明native方法,传入一个整型数组和一个字符串
public native int processArrayAndString(int[] numbers, String text);
static {
System.loadLibrary("example");
}
}
对应的C代码:
#include <jni.h>
#include <stdio.h>
#include "ExampleJNI.h"
JNIEXPORT jint JNICALL Java_ExampleJNI_processArrayAndString(JNIEnv *env, jobject obj, jintArray numbers, jstring text) {
// 处理整型数组
jint *numArray = (*env)->GetIntArrayElements(env, numbers, NULL);
jsize length = (*env)->GetArrayLength(env, numbers);
// 处理字符串
const char *nativeString = (*env)->GetStringUTFChars(env, text, NULL);
// 示例处理:将数组所有元素相加
int sum = 0;
for (int i = 0; i < length; i++) {
sum += numArray[i];
}
printf("Received string: %s\n", nativeString);
printf("Sum of array: %d\n", sum);
// 释放内存
(*env)->ReleaseIntArrayElements(env, numbers, numArray, 0);
(*env)->ReleaseStringUTFChars(env, text, nativeString);
return sum;
}
在这个示例中:
jintArray
用于接收Java的int[]
数组,并使用GetIntArrayElements
读取内容。jstring
用于接收Java字符串,并使用GetStringUTFChars
将其转换为C字符串。
JavaVM
jdk 中关于 JavaVM 的定义代码如下(JDK - JNIInvokeInterface_):
struct JNIInvokeInterface_;
struct JavaVM_;
#ifdef __cplusplus
typedef JavaVM_ JavaVM;
#else
typedef const struct JNIInvokeInterface_ *JavaVM;
#endif
struct JNIInvokeInterface_ {
void *reserved0;
void *reserved1;
void *reserved2;
jint (JNICALL *DestroyJavaVM)(JavaVM *vm);
jint (JNICALL *AttachCurrentThread)(JavaVM *vm, void **penv, void *args);
jint (JNICALL *DetachCurrentThread)(JavaVM *vm);
jint (JNICALL *GetEnv)(JavaVM *vm, void **penv, jint version);
jint (JNICALL *AttachCurrentThreadAsDaemon)(JavaVM *vm, void **penv, void *args);
};
struct JavaVM_ {
const struct JNIInvokeInterface_ *functions;
#ifdef __cplusplus
jint DestroyJavaVM() {
return functions->DestroyJavaVM(this);
}
jint AttachCurrentThread(void **penv, void *args) {
return functions->AttachCurrentThread(this, penv, args);
}
jint DetachCurrentThread() {
return functions->DetachCurrentThread(this);
}
jint GetEnv(void **penv, jint version) {
return functions->GetEnv(this, penv, version);
}
jint AttachCurrentThreadAsDaemon(void **penv, void *args) {
return functions->AttachCurrentThreadAsDaemon(this, penv, args);
}
#endif
};
在上述的头文件中可以看出 JavaVM 是用于获取 JNIEnv、附加/分离线程
需要注意的是 jni 的整体设计是基于 C 的,而不是 C++,所以并没有面向对象的概念。JavaVM 可以理解成一个抽象接口,表示 java 虚拟机实例,但是实现并非是以面向对象的接口形式来实现。
JavaVM 是全局的,一个进程(通常)只有一个 JavaVM 示例
- 在 Java 环境中,JavaVM 是已经被创建的,所以不需要再此创建
- 在 native 环境中,如果调用 Java 代码,则需要调用
JNI_CreateJavaVM
手动创建 JavaVM - JavaVM 用于关联 JNIEnv 与线程
- 函数指针表的绑定是通过 jni_InvokeInterface
关键“成员变量”的挂在点:
成员变量 | 挂载位置 | 文件路径 | 描述 |
---|---|---|---|
线程管理 (Threads) | Threads::_main_vm_thread |
src/hotspot/share/runtime/thread.cpp |
管理所有 JVM 线程,包括主线程和附加线程。 |
JNI 环境 (JNIEnv) | JavaThread::_jni_environment |
src/hotspot/share/runtime/thread.hpp |
每个线程的 JNI 环境,用于与 Java 交互。 |
类加载器 (ClassLoaderData) | ClassLoaderData::_class_loader |
src/hotspot/share/classfile/classLoaderData.hpp |
管理类加载器的数据,包括方法区的类定义。 |
内存管理 (Heap) | Universe::_collectedHeap |
src/hotspot/share/memory/universe.cpp |
管理 JVM 的堆内存,包括垃圾回收。 |
全局配置 (Arguments) | Arguments 单例 |
src/hotspot/share/runtime/arguments.cpp |
JVM 启动时解析的全局参数,控制 JVM 行为。 |
JNIEnv
jdk 中关于 JNIEnv 的定义代码简要如下(JDK - JNINativeInterface_):
struct JNINativeInterface_;
struct JNIEnv_;
#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif
struct JNINativeInterface_ {
// 仅列出使用频率高的
...
jint (JNICALL *GetVersion)(JNIEnv *env);
jclass (JNICALL *DefineClass) (JNIEnv *env, const char *name, jobject loader, const jbyte *buf, jsize len);
jclass (JNICALL *FindClass) (JNIEnv *env, const char *name);
jmethodID (JNICALL *GetMethodID) (JNIEnv *env, jclass clazz, const char *name, const char *sig);
jobject (JNICALL *GetObjectField) (JNIEnv *env, jobject obj, jfieldID fieldID);
jboolean (JNICALL *GetBooleanField) (JNIEnv *env, jobject obj, jfieldID fieldID);
jbyte (JNICALL *GetByteField) (JNIEnv *env, jobject obj, jfieldID fieldID);
jstring (JNICALL *NewString) (JNIEnv *env, const jchar *unicode, jsize len);
jsize (JNICALL *GetStringLength) (JNIEnv *env, jstring str);
const jchar *(JNICALL *GetStringChars) (JNIEnv *env, jstring str, jboolean *isCopy);
void (JNICALL *ReleaseStringChars) (JNIEnv *env, jstring str, const jchar *chars);
jstring (JNICALL *NewStringUTF) (JNIEnv *env, const char *utf);
jsize (JNICALL *GetStringUTFLength) (JNIEnv *env, jstring str);
const char* (JNICALL *GetStringUTFChars) (JNIEnv *env, jstring str, jboolean *isCopy);
void (JNICALL *ReleaseStringUTFChars) (JNIEnv *env, jstring str, const char* chars);
jsize (JNICALL *GetArrayLength) (JNIEnv *env, jarray array);
jobjectArray (JNICALL *NewObjectArray) (JNIEnv *env, jsize len, jclass clazz, jobject init);
jobject (JNICALL *GetObjectArrayElement) (JNIEnv *env, jobjectArray array, jsize index);
jbooleanArray (JNICALL *NewBooleanArray) (JNIEnv *env, jsize len);
jbyteArray (JNICALL *NewByteArray) (JNIEnv *env, jsize len);
jobject (JNICALL *GetStaticObjectField) (JNIEnv *env, jclass clazz, jfieldID fieldID);
jboolean (JNICALL *GetStaticBooleanField) (JNIEnv *env, jclass clazz, jfieldID fieldID);
...
};
同 JavaVM,JNIEnv 是 JNINativeInterface_ 结构体的指针,而 JNINativeInterface_ 则是一个函数指针表
- JNIEnv 的函数指针表的绑定是通过 jni_functions
- 通过
create_jni_environment
来创建 JNIEnv,并为 functions 赋值 - 具体的 JNIEnv 则是存储在 JavaThread 中的 _jni_environment
- 既然是线程的成员变量,则每个线程的 JNIEnv 必然也是独立的,线程安全的
- 在 AttachCurrentThread 时,会创建该线程的 JNIEnv
native 是怎么拿到 Java 变量的
示例:
public class MyObject {
private String name;
public MyObject(String name) {
this.name = name;
}
public native void printName();
}
// 实现 native 方法,为了展示逻辑,无各种边界判断
JNIEXPORT void JNICALL Java_MyObject_printName(JNIEnv *env, jobject obj) {
jclass clazz = (*env)->GetObjectClass(env, obj); // 获取类对象
jfieldID fid = (*env)->GetFieldID(env, clazz, "name", "Ljava/lang/String;"); // 获取字段的 Field ID
jstring name = (jstring)(*env)->GetObjectField(env, obj, fid); // 获取字段值(jstring 类型)
const char *c_name = (*env)->GetStringUTFChars(env, name, NULL); // 将 jstring 转换为 C 字符串
printf("Name: %s\n", c_name); // 打印字段值
(*env)->ReleaseStringUTFChars(env, name, c_name); // 释放资源
}
上述示例中,看起来好像挺像回事,但是 native 具体是怎么夸 runtime 拿到具体变量值的又没展示出来。(个人认为)核心是 GetObjectField,这里有两个关键点:
- jstring 到底是什么
- GetObjectField 具体怎么实现
jstring 到底是什么
typedef jobject jstring;
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
typedef _jobject *jobject;
typedef _jclass *jclass;
typedef _jthrowable *jthrowable;
typedef _jstring *jstring;
- 由上可以看出,jstring、jobject 均是一个空对象的指针
- GetObjectField 的核心是通过 fieldID 和偏移量,在对象的内存中快速定位字段值
- fieldID 的解析和偏移量计算是类加载时完成的
- JVM 通过句柄表和偏移量确保操作安全且高效
- JVM 使用句柄来隔离 native 层和 Java 堆内存的直接交互,保证内存管理的安全性和垃圾回收的一致性
- jobject 句柄的管理是通过句柄表(Handle Table)完成的,核心模块是 Handles
- 本地句柄表结构为 HandleArea,挂载于 JavaThread::_handle_area,
- 全局句柄表结构为 OopStorage,挂载于 GlobalHandles::_oop_storage