上一篇: 04-JNI函数
调用 API 允许软件供应商将 Java VM 加载到任意本地应用程序中。供应商可以提供支持 Java 的应用程序,而无需链接 Java VM 源代码。
5.1 概述
下面的代码示例说明了如何使用调用 API 中的函数。在这个示例中,C++ 代码创建了一个 Java VM 并调用了一个名为 Main.test 的静态方法。为清晰起见,我们省略了错误检查。
#include <jni.h> /* where everything is defined */
...
JavaVM *jvm; /* denotes a Java VM */
JNIEnv *env; /* pointer to native method interface */
JavaVMInitArgs vm_args; /* JDK/JRE 19 VM initialization arguments */
JavaVMOption* options = new JavaVMOption[1];
options[0].optionString = "-Djava.class.path=/usr/lib/java";
vm_args.version = JNI_VERSION_19;
vm_args.nOptions = 1;
vm_args.options = options;
vm_args.ignoreUnrecognized = false;
/* load and initialize a Java VM, return a JNI interface
* pointer in env */
JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
delete options;
/* invoke the Main.test method using the JNI */
jclass cls = env->FindClass("Main");
jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V");
env->CallStaticVoidMethod(cls, mid, 100);
/* We are done. */
jvm->DestroyJavaVM();
本示例使用了 API 中的两个函数。调用 API 允许本地应用程序使用 JNI 接口指针访问虚拟机功能。
5.1.1 创建虚拟机
JNI_CreateJavaVM() 函数加载并初始化 Java VM,并返回一个指向 JNI 接口指针的指针。调用 JNI_CreateJavaVM() 的线程被视为主线程,并连接到 Java VM。
注意:根据操作系统的不同,原始进程线程可能会受到特殊处理,从而影响其作为正常 Java 线程正常运行的能力(如堆栈大小受限和可以抛出 StackOverflowError )。强烈建议不要使用原始线程加载 Java VM,而应专门为此创建一个新线程。
5.1.2 附加到虚拟机
JNI 接口指针( JNIEnv )只在当前线程中有效。如果另一个线程需要访问 Java 虚拟机,它必须首先调用 AttachCurrentThread() 将自己附加到虚拟机并获取 JNI 接口指针。附加到虚拟机后,本地线程的工作方式与运行在本地方法中的普通 Java 线程无异,唯一的例外是在调用对调用者敏感的方法时没有 Java 调用者。本地线程会一直附着在虚拟机上,直到调用 DetachCurrentThread() 将自己分离。
附加线程应有足够的堆栈空间来执行合理的工作。每个线程的堆栈空间分配取决于操作系统。例如,使用 pthreads 时,可以在 pthread_create 的 pthread_attr_t 参数中指定堆栈大小。
5.1.3 脱离虚拟机
连接到虚拟机的本地线程必须在终止前调用 DetachCurrentThread() 来自行分离。如果调用栈上有 Java 方法,线程则无法自行退出。
5.1.4 终止虚拟机
DestroyJavaVM() 功能可终止 Java 虚拟机。
该函数会等到没有非守护进程线程执行时才实际终止虚拟机。非守护进程线程包括: Java 线程和附加的本地线程。等待的原因是非守护进程线程可能会占用系统资源,如锁或窗口。Java 应用程序或附加本地代码的程序员有责任在线程终止或分离前释放这些资源。虚拟机无法自动释放这些资源,因此会等待程序员在终止前释放这些资源。
5.2 库和版本管理
本地程序库可以与虚拟机动态链接或静态链接。库与虚拟机映像的结合方式取决于实现。必须使用 System.loadLibrary 或等效的 API 才能将库视为已加载,这既适用于动态链接的库,也适用于静态链接的库。
本地库一旦加载,所有类加载器都能看到它。因此,不同类加载器中的两个类可能会链接到同一个本地方法。这会导致两个问题:
①. 一个类可能会错误地与不同类加载器中同名类加载的本地库链接。
②. 本地方法很容易混合来自不同类加载器的类。这会破坏类加载器提供的名称空间分隔,并导致类型安全问题。
每个类加载器都管理自己的本地库。同一个 JNI 本地库不能加载到多个类加载器中。这样做会导致 UnsatisfiedLinkError 抛出。例如,当使用 System.loadLibrary 将一个本地库加载到两个类加载器时,就会抛出 UnsatisfiedLinkError 。这种方法的优点是:
①. 本地程序库中保留了基于类加载器的名称空间分隔。本地库不能轻易混合来自不同类加载器的类。
②. 此外,当相应的类加载器被垃圾回收时,本地库也可以被卸载。
5.2.1 支持静态链接库
以下规则适用于静态链接库,这些示例中给出的静态链接库名为 "L":
①. 当且仅当库导出一个名为 JNI_OnLoad_L的函数时,其映像已与虚拟机结合的库 "L "才被定义为静态链接库。
②. 如果静态链接库 L 输出了一个名为 JNI_OnLoad_L 的函数和一个名为 JNI_OnLoad 的函数,那么 JNI_OnLoad 函数将被忽略。
③. 如果函数库 L 是静态链接的,那么在首次调用 System.loadLibrary("L") 或等效 API 时,将调用一个 JNI_OnLoad_L 函数,其参数和预期返回值与为 JNI_OnLoad函数指定的参数和预期返回值相同。
④. 静态链接的库 L 将禁止动态加载同名库。
⑤. 当包含静态链接本地程序库 L 的类加载器被垃圾回收时,如果程序库导出了 JNI_OnUnload_L函数,虚拟机将调用该函数。
⑥. 如果静态链接库 L 输出了一个名为 JNI_OnUnload_L 的函数和一个名为 JNI_OnUnload 的函数,那么 JNI_OnUnload 函数将被忽略。
程序员还可以调用 JNI 函数 RegisterNatives() 来注册与类相关的本地方法。 RegisterNatives() 函数对静态链接函数特别有用。
如果动态链接库定义了 JNI_OnLoad_L 和/或JNI_OnUnload_L函数,这些函数将被忽略。
5.2.2 库钩子函数的生命周期
为了便于版本控制和资源管理,JNI 库可以定义加载和卸载函数钩子。这些函数的命名取决于库是动态链接还是静态链接。
5.2.3 JNI_OnLoad
/**
* @brief 动态链接库定义的可选函数。
* 虚拟机在加载本地库时调用 JNI_OnLoad (例如通过 System.loadLibrary )
*
* @param vm 指向当前虚拟机结构的指针
* @param reserved 未使用的指针
* @return jint 返回所需的 JNI_VERSION 常量(另见 GetVersion )
*/
jint JNI_OnLoad(JavaVM *vm, void *reserved);
为了使用在 JNI API 的某个版本中定义的函数, JNI_OnLoad 必须返回一个至少定义了该版本的常量。例如,希望使用 JDK 1.4 引入的1 个函数的库至少需要返回 JNI_VERSION_1_4 个常量。如果本地库没有导出 JNI_OnLoad 个函数,虚拟机就会认为该库只需要 JNI 版本 JNI_VERSION_1_1 。如果虚拟机无法识别 JNI_OnLoad 所返回的版本号,虚拟机将卸载该库,就像从未加载过该库一样。
从包含本地方法实现的动态链接本地库导出。
5.2.4 JNI_OnUnload
/**
* @brief 动态链接库定义的可选函数。
* 当包含本地库的类加载器被垃圾回收时,虚拟机会调用 JNI_OnUnload 。
*
* @param vm 指向当前虚拟机结构的指针
* @param reserved 未使用的指针
*/
void JNI_OnUnload(JavaVM *vm, void *reserved);
该函数可用于执行清理操作。由于该函数是在未知上下文(如来自 finalizer)中调用的,因此程序员在使用 Java VM 服务时应保持谨慎,避免任意回调 Java 服务。
从包含本地方法实现的动态链接本地库导出。
5.2.5 JNI_OnLoad_L
/**
* @brief 静态链接库必须定义的强制函数
*
* @param vm 指向当前虚拟机结构的指针
* @param reserved 未使用的指针
* @return jint 返回所需的 JNI_VERSION 常量(另见 GetVersion )。返回的最小版本至少为 JNI_VERSION_1_8
* @since JDK/JRE 1.8
*/
jint JNI_Onload_<L>(JavaVM *vm, void *reserved);
如果一个名为 "L "的库是静态链接的,那么在首次调用 System.loadLibrary("L") 或等效 API 时,将调用一个 JNI_OnLoad_L 函数,其参数和预期返回值与为 JNI_OnLoad 函数指定的参数和预期返回值相同。 JNI_OnLoad_L 必须返回本地库所需的 JNI 版本。该版本必须是 JNI_VERSION_1_8 或更高版本。如果虚拟机无法识别 JNI_OnLoad_L 所返回的版本号,虚拟机将把该库当作从未加载过。
5.2.6 JNI_OnUnload_L
/**
* @brief 静态链接库定义的可选函数。
* 当包含静态链接本地程序库 "L "的类加载器被垃圾回收时,如果程序库导出了 JNI_OnUnload_L 函数,虚拟机将调用该函数。
* @param vm 指向当前虚拟机结构的指针
* @param reserved 未使用的指针
*/
void JNI_OnUnload_<L>(JavaVM *vm, void *reserved);
该函数可用于执行清理操作。由于该函数是在未知上下文(如来自 finalizer)中调用的,因此程序员在使用 Java VM 服务时应保持谨慎,避免任意回调 Java 服务。
从包含本地方法实现的静态链接本地库中导出。
信息说明:
加载本地库的行为是使 Java 虚拟机和运行时了解并注册该库及其本地入口点的完整过程。请注意,仅仅执行操作系统级别的操作来加载本地库(如在 UNIX(R) 系统上执行 dlopen 操作)并不能完全实现这一目标。本机函数通常由 Java 类加载器调用,以执行对主机操作系统的调用,将库加载到内存中,并返回本机库的句柄。该句柄将被存储并用于后续本地库入口点的搜索。一旦句柄成功返回,Java 本地类加载器将完成加载过程,并注册该库。
5.3 调用 API 函数
JavaVM 类型是指向调用 API 函数表的指针。下面的代码示例显示了该函数表:
typedef const struct JNIInvokeInterface *JavaVM;
const struct JNIInvokeInterface ... = {
NULL,
NULL,
NULL,
DestroyJavaVM,
AttachCurrentThread,
DetachCurrentThread,
GetEnv,
AttachCurrentThreadAsDaemon
};
请注意, JNI_GetDefaultJavaVMInitArgs() 、 JNI_GetCreatedJavaVMs() 和 JNI_CreateJavaVM() 这三个调用 API 函数不在 JavaVM 函数表中。这些函数可以在没有预先存在的 JavaVM 结构的情况下使用。
5.3.1 JNI_GetDefaultJavaVMInitArgs
/**
* @brief 返回 Java 虚拟机的默认配置。
* 调用此函数前,本地代码必须将 vm_args->version 字段设置为它期望虚拟机支持的 JNI 版本。
* 此函数返回后,vm_args->version 将被设置为 VM 实际支持的 JNI 版本。
*
* @param vm_args 指向 JavaVMInitArgs 结构的指针,缺省参数填入该结构中,不能是 NULL 结构
* @return jint 如果请求的版本受支持,则返回 JNI_OK ;如果请求的版本不受支持,则返回 JNI 错误代码(负数)。
*/
jint JNI_GetDefaultJavaVMInitArgs(void *vm_args);
从实现 Java 虚拟机的本地库中导出。
5.3.2 JNI_GetCreatedJavaVMs
/**
* @brief 返回已创建的所有 Java 虚拟机。
* 虚拟机的指针将按照创建顺序写入缓冲区 vmBuf 。最多写入1 个条目。
* 创建的虚拟机总数将在 \*nVMs 中返回。
*
* @param vmBuf 指向放置虚拟机结构的缓冲区的指针,不得为 NULL
* @param bufLen 缓冲区的长度
* @param nVMs 整数指针。可为 NULL 值
* @return jint 成功时返回 JNI_OK ;失败时返回合适的 JNI 错误代码(负数)。
*/
jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs);
5.3.3 JNI_CreateJavaVM
/**
* @brief 加载并初始化 Java VM。
* 当前线程将连接到 Java VM 并成为主线程。将 p_env 参数设置为主线程的 JNI 接口指针。
*
* @param p_vm 指针,指向放置生成的虚拟机结构的位置。不能为 NULL
* @param p_env 指向主线程 JNI 接口指针位置的指针。不能为 NULL
* @param vm_args Java VM 初始化参数。不能为 NULL
* @return jint 成功时返回 JNI_OK ;失败时返回合适的 JNI 错误代码(负数)。
*/
jint JNI_CreateJavaVM(JavaVM **p_vm, void **p_env, void *vm_args);
不支持在单个进程中创建多个虚拟机。
JNI_CreateJavaVM 的第二个参数始终是指向 JNIEnv * 的指针,而第三个参数是指向 JavaVMInitArgs 结构的指针,该结构使用选项字符串对任意虚拟机启动选项进行编码:
typedef struct JavaVMInitArgs {
jint version;
jint nOptions;
JavaVMOption *options;
jboolean ignoreUnrecognized;
} JavaVMInitArgs;
options 字段是以下类型的数组:
typedef struct JavaVMOption {
char *optionString; /* the option as a string in the default platform encoding */
void *extraInfo;
} JavaVMOption;
数组的大小由 JavaVMInitArgs 中的 nOptions 字段表示。如果 ignoreUnrecognized 为 JNI_TRUE , JNI_CreateJavaVM 会忽略所有以 " -X "或 " _ "开头的未识别选项字符串。如果 ignoreUnrecognized 为 JNI_FALSE , JNI_CreateJavaVM 在遇到任何未识别的选项字符串时立即返回 JNI_ERR 。所有 Java 虚拟机都必须识别以下一组标准选项:
与模块相关的选项 --add-reads 、 --add-exports 、 --add-opens 、 --add-modules 、 --limit-modules 、 --module-path 、 --patch-module 和 --upgrade-module-path 必须以选项字符串形式传递,使用 "option=value "格式,而不是 "option value "格式(注意,"option "和 "value "之间必须有 " = "。)例如,要将 java.management/sun.management 输出为 ALL-UNNAMED ,必须传递选项字符串 "--add-exports=java.management/sun.management=ALL-UNNAMED" 。
此外,每个虚拟机实现都可能支持自己的一套非标准选项字符串。非标准选项名称必须以 " -X "或下划线(" _ ")开头。例如,JDK/JRE 支持 -Xms 和 -Xmx 选项,允许程序员指定初始堆大小和最大堆大小。以 " -X "开头的选项可以通过 " java "命令行访问。
下面是在 JDK/JRE 中创建 Java VM 的示例代码:
JavaVMInitArgs vm_args;
JavaVMOption options[3];
options[0].optionString = "-Djava.class.path=c:\myclasses"; /* user classes */
options[1].optionString = "-Djava.library.path=c:\mylibs"; /* set native library path */
options[2].optionString = "-verbose:jni"; /* print JNI-related messages */
vm_args.version = JNI_VERSION_1_2;
vm_args.options = options;
vm_args.nOptions = 3;
vm_args.ignoreUnrecognized = TRUE;
/* Note that in the JDK/JRE, there is no longer any need to call
* JNI_GetDefaultJavaVMInitArgs.
*/
res = JNI_CreateJavaVM(&vm, (void **)&env, &vm_args);
if (res < 0) ...
5.3.4 DestroyJavaVM
/**
* @brief 终止 Java 虚拟机的运行,尽力释放虚拟机资源。
* JavaVM 接口函数表中的索引 3
*
* @param vm 将被销毁的 Java 虚拟机。不能为 NULL
* @return jint 成功时返回 JNI_OK ;失败时返回合适的 JNI 错误代码(负数)
*/
jint DestroyJavaVM(JavaVM *vm);
任何线程,无论是否已连接,都可以调用此函数。如果当前线程尚未附加,则首先附加该线程。如果当前线程已被附加,那么如果它的调用栈中有任何 Java 方法,则会出错。
该函数等待所有非守护进程线程(如果当前线程为非守护进程线程,则不包括该线程)都已终止,然后启动关闭序列(请参阅 java.lang.Runtime)。关闭序列结束时,Java VM 将终止,导致任何仍在执行 Java 代码的线程停止执行该代码,并释放其能够释放的任何相关 VM 资源。此时,当前线程不再依附于 Java VM,此函数返回给调用者。
请注意,Java VM 终止时仍在执行本地代码的任何线程都将继续执行该代码;但如果它们试图恢复执行 Java 代码,则将停止执行。这包括守护进程线程和关闭序列启动后启动的任何非守护进程线程。守护进程线程和非守护进程线程仅对附加的本地线程有意义;非附加的本地线程不受 Java VM 终止的影响。
5.3.5 AttachCurrentThread
/**
* @brief 将当前线程作为非守护进程线程附加到 Java 虚拟机。在 p_env 参数中返回一个 JNI 接口指针
* JavaVM 接口函数表中的索引 4
*
* @param vm 当前线程将连接的虚拟机,必须不是 NULL
* @param p_env 指向当前线程的 JNI 接口指针所在位置的指针。不能为 NULL
* @param thr_args 可以是 NULL 或指向 JavaVMAttachArgs 结构的指针,用于指定附加信息
* @return jint 成功时返回 JNI_OK ;失败时返回合适的 JNI 错误代码(负数)
*/
jint AttachCurrentThread(JavaVM *vm, void **p_env, void *thr_args);
尝试附加已附加的线程时,只会在 p_env 参数中返回其现有的 JNI 接口指针。调用此方法后,已附加线程的守护进程状态将保持不变。
一个本地线程不能同时连接到两个 Java 虚拟机。
当线程连接到虚拟机时,上下文类加载器就是引导加载器。
thr_args:可以是 NULL 或指向 JavaVMAttachArgs 结构的指针,用于指定附加信息:
typedef struct JavaVMAttachArgs {
jint version;
char *name; /* the name of the thread as a modified UTF-8 string, or NULL */
jobject group; /* global ref of a ThreadGroup object, or NULL */
} JavaVMAttachArgs
5.3.6 AttachCurrentThreadAsDaemon
/**
* @brief 将当前线程作为守护线程附加到 Java VM。在 p_env 参数中返回一个 JNI 接口指针
* JavaVM 接口函数表中的索引 7
*
* @param vm 当前线程将连接的虚拟机实例。不能为 NULL
* @param p_env 指向当前线程 JNIEnv 接口指针所在位置的指针。它不能是 NULL
* @param thr_args 可以是 NULL 或指向 JavaVMAttachArgs 结构的指针,用于指定附加信息
* @return jint 成功时返回 JNI_OK ;失败时返回合适的 JNI 错误代码(负数)
*/
jint AttachCurrentThreadAsDaemon(JavaVM *vm, void **p_env, void *thr_args);
尝试附加已附加的线程时,只会在 p_env 参数中返回其现有的 JNI 接口指针。调用此方法后,已附加线程的守护进程状态将保持不变。
一个本地线程不能同时连接到两个 Java 虚拟机。
当线程连接到虚拟机时,上下文类加载器就是引导加载器。
thr_args :可以是 NULL 或指向 JavaVMAttachArgs 结构的指针,用于指定附加信息:
typedef struct JavaVMAttachArgs {
jint version;
char *name; /* the name of the thread as a modified UTF-8 string, or NULL */
jobject group; /* global ref of a ThreadGroup object, or NULL */
} JavaVMAttachArgs
5.3.7 DetachCurrentThread
/**
* @brief 将当前线程从 Java VM 中分离。如果调用栈中有 Java 方法,则线程无法脱离。
* JavaVM 接口函数表中的索引 5
*
* @param vm 当前线程将脱离的虚拟机。必须不是 NULL
* @return jint 成功时返回 JNI_OK ;失败时返回合适的 JNI 错误代码(负数)
*/
jint DetachCurrentThread(JavaVM *vm);
该线程仍持有的任何 Java 监视器都会被释放(不过在正确编写的程序中,所有监视器都会在此之前被释放)。现在,该线程被视为已终止,不再存活;所有等待该线程死亡的 Java 线程都会收到通知。
主线程可以从虚拟机中分离出来。
试图分离未连接的线程是不可能的。
如果调用 DetachCurrentThread 时有异常等待处理,虚拟机可以选择报告该异常的存在。
5.3.8 GetEnv
/**
* @brief JavaVM 接口函数表中的索引 6
*
* @param vm 将从中检索接口的虚拟机实例。不得为 NULL
* @param p_env 指向当前线程的 JNI 接口指针所在位置的指针。不能为 NULL
* @param version 请求的 JNI 版本
* @return jint 如果当前线程未连接到虚拟机,则将 *env 设为 NULL ,并返回 JNI_EDETACHED 。
* 如果不支持指定的版本,则将 *env 设置为 NULL ,并返回 JNI_EVERSION 。
* 否则,将 *env 设置为相应的接口,并返回 JNI_OK
*/
jint GetEnv(JavaVM *vm, void **p_env, jint version);