第五章:调用API

调用API允许软件供应商将Java虚拟机加载到任意本机应用程序中。供应商可以交付支持Java的应用程序,而无需链接Java虚拟机源代码。

本章首先概述了调用API。接下来是所有调用API函数的参考页面。它涵盖以下主题:

概述

以下代码示例说明了如何使用调用API中的函数。在此示例中,C++代码创建了一个Java虚拟机,并调用了一个名为Main.test的静态方法。为了清晰起见,我们省略了错误检查。

#include <jni.h>       /* 定义所有内容的位置 */
...
JavaVM *jvm;       /* 表示Java虚拟机 */
JNIEnv *env;       /* 指向本机方法接口的指针 */
JavaVMInitArgs vm_args; /* JDK/JRE 19 VM初始化参数 */
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;
/* 加载和初始化Java虚拟机,返回一个JNI接口指针
 * 在env中 */
JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
delete options;
/* 使用JNI调用Main.test方法 */
jclass cls = env->FindClass("Main");
jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V");
env->CallStaticVoidMethod(cls, mid, 100);
/* 完成。 */
jvm->DestroyJavaVM();

此示例使用了API中的两个函数。调用API允许本机应用程序使用JNI接口指针访问VM功能。

创建虚拟机

JNI_CreateJavaVM()函数加载并初始化Java虚拟机,并返回一个JNI接口指针。调用JNI_CreateJavaVM()的线程被视为主线程,并附加到Java虚拟机。

注意:根据操作系统的不同,原始进程线程可能会受到特殊处理,影响其作为正常Java线程的功能(例如具有有限的堆栈大小并能够抛出StackOverflowError)。强烈建议不要使用原始线程来加载Java虚拟机,而是为此目的创建一个新线程。

附加到虚拟机

JNI接口指针(JNIEnv)仅在当前线程中有效。如果另一个线程需要访问Java虚拟机,它必须首先调用AttachCurrentThread()将自己附加到虚拟机并获取JNI接口指针。一旦附加到虚拟机,本机线程就像在本机方法中运行的普通Java线程一样,唯一的例外是在调用DetachCurrentThread()时没有Java调用者时调用caller-sensitive methods。本机线程保持附加到虚拟机,直到调用DetachCurrentThread()将其分离。

附加的线程应该有足够的堆栈空间来执行合理数量的工作。每个线程的堆栈空间分配是特定于操作系统的。例如,使用pthread时,堆栈大小可以在pthread_createpthread_attr_t参数中指定。

从虚拟机分离

附加到虚拟机的本机线程在终止之前必须调用DetachCurrentThread()将自己分离。如果调用堆栈上有Java方法,则线程无法分离自己。

终止虚拟机

DestroyJavaVM()函数终止Java虚拟机。

该函数会等待直到没有非守护线程在执行,然后才实际终止虚拟机。非守护线程包括Java线程和附加的本机线程。等待的原因是非守护线程可能持有系统资源,如锁定或窗口。Java应用程序或附加的本机代码的程序员负责在线程终止或分离之前释放这些资源。虚拟机无法自动释放它们,因此它等待程序员在终止之前释放它们。

库和版本管理

本机库可以是动态链接的,也可以是静态链接的。库和VM映像的组合方式取决于实现。要考虑库被加载,必须成功执行System.loadLibrary或等效API,这适用于动态链接库和静态链接库。

一旦加载了本机库,它就可以从所有类加载器中访问。因此,不同类加载器中的两个类可能链接到相同的本机方法。这会导致两个问题:

每个类加载器管理自己的一组本机库。相同的JNI本机库不能加载到多个类加载器中。这样做会导致抛出UnsatisfiedLinkError。例如,当使用System.loadLibrary将本机库加载到两个类加载器中时,会抛出UnsatisfiedLinkError。这种方法的好处包括:

静态链接库支持

对于这些示例中给出的静态链接库,以下规则适用于静态链接库'L':

程序员还可以调用JNI函数RegisterNatives()来注册与类关联的本机方法。RegisterNatives()函数在静态链接函数中特别有用。

如果动态链接库定义了JNI_OnLoad_L和/或JNI_OnUnload_L函数,则这些函数将被忽略。

库生命周期函数挂钩

为了促进版本控制和资源管理,JNI库可以定义加载卸载函数挂钩。这些函数的命名取决于库是动态链接还是静态链接。


JNI_OnLoad

jint JNI_OnLoad(JavaVM *vm, void *reserved);

由动态链接库定义的可选函数。当本机库被加载时(例如,通过System.loadLibrary),VM会调用JNI_OnLoad

为了使用某个版本JNI API中定义的函数,JNI_OnLoad必须返回至少定义该版本的常量。例如,希望使用JDK 1.4中引入的AttachCurrentThreadAsDaemon函数的库需要返回至少JNI_VERSION_1_4。如果本机库不导出JNI_OnLoad函数,则VM假定该库仅需要JNI版本JNI_VERSION_1_1。如果VM不识别JNI_OnLoad返回的版本号,则VM将卸载该库,并表现得好像从未加载过该库。

链接:

从包含本机方法实现的动态链接本机库中导出。

参数:

vm:指向当前VM结构的指针。

reserved:未使用的指针。

返回:

返回所需的JNI_VERSION常量(另请参见GetVersion)。


JNI_OnUnload

void JNI_OnUnload(JavaVM *vm, void *reserved);

由动态链接库定义的可选函数。当包含本机库的类加载器被垃圾回收时,VM会调用JNI_OnUnload

此函数可用于执行清理操作。由于此函数在未知上下文中调用(例如从终结器调用),程序员在使用Java VM服务时应保守,并避免任意的Java回调。

链接:

从包含本机方法实现的动态链接本机库中导出。

参数:

vm:指向当前VM结构的指针。

reserved:未使用的指针。


JNI_OnLoad_L

jint JNI_Onload_<L>(JavaVM *vm, void *reserved);

必须由静态链接库定义的强制性函数。

如果一个名为'L'的库是静态链接的,那么在第一次调用System.loadLibrary("L")或等效的API时,将调用一个具有与JNI_OnLoad函数指定的相同参数和期望返回值的JNI_OnLoad_L函数。JNI_OnLoad_L必须返回本地库所需的JNI版本。此版本必须是JNI_VERSION_1_8或更高版本。如果VM不识别JNI_OnLoad_L返回的版本号,则VM将表现得好像从未加载过该库。

链接:

从包含本地方法实现的静态链接本地库中导出。

参数:

vm:指向当前VM结构的指针。

reserved:未使用的指针。

返回:

返回所需的JNI_VERSION常量(另请参阅GetVersion)。返回的最低版本至少为JNI_VERSION_1_8

自:

JDK/JRE 1.8


JNI_OnUnload_L

void JNI_OnUnload_<L>(JavaVM *vm, void *reserved);

由静态链接库定义的可选函数。当包含静态链接本地库'L'的类加载器被垃圾回收时,如果导出了JNI_OnUnload_L函数,则VM将调用该库的JNI_OnUnload_L函数。

此函数可用于执行清理操作。由于此函数在未知上下文中被调用(例如从终结器调用),因此程序员在使用Java VM服务时应保守,并避免任意的Java回调。

链接:

从包含本地方法实现的静态链接本地库中导出。

参数:

vm:指向当前VM结构的指针。

reserved:未使用的指针。

自:

JDK/JRE 1.8

信息注释:

加载本地库的行为是使库及其本地入口点完全知晓并注册到Java VM和运行时的完整过程。请注意,仅执行操作系统级别的操作来加载本地库,例如在UNIX(R)系统上的dlopen,并不能完全实现此目标。通常从Java类加载器调用本地函数,以执行调用主机操作系统的操作,将库加载到内存中并返回一个句柄给本地库。此句柄将被存储并用于后续搜索本地库入口点。一旦成功返回句柄以注册库,Java本地类加载器将完成加载过程。


调用API函数

JavaVM类型是指向调用API函数表的指针。以下代码示例显示了这个函数表。

typedef const struct JNIInvokeInterface *JavaVM;

const struct JNIInvokeInterface ... = {
    NULL,
    NULL,
    NULL,

    DestroyJavaVM,
    AttachCurrentThread,
    DetachCurrentThread,

    GetEnv,

    AttachCurrentThreadAsDaemon
};

请注意,三个调用API函数JNI_GetDefaultJavaVMInitArgs()JNI_GetCreatedJavaVMs()JNI_CreateJavaVM()不是JavaVM函数表的一部分。这些函数可以在没有预先存在的JavaVM结构的情况下使用。


JNI_GetDefaultJavaVMInitArgs

jint JNI_GetDefaultJavaVMInitArgs(void *vm_args);

返回Java VM的默认配置。在调用此函数之前,本机代码必须将vm_args->version字段设置为它期望VM支持的JNI版本。此函数返回后,vm_args->version将设置为VM支持的实际JNI版本。

链接:

从实现Java虚拟机的本地库中导出。

参数:

vm_args:指向JavaVMInitArgs结构的指针,其中填充了默认参数,不能为NULL

返回:

如果支持请求的版本,则返回JNI_OK;如果不支持请求的版本,则返回JNI错误代码(负数)。


JNI_GetCreatedJavaVMs

jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs);

返回已创建的所有Java VM。将VM指针按创建顺序写入缓冲区vmBuf。最多将写入bufLen个条目。已创建的VM总数将在\*nVMs中返回。

不支持在单个进程中创建多个VM。

链接:

从实现Java虚拟机的本地库中导出。

参数:

vmBuf:指向将放置VM结构的缓冲区的指针,不能为NULL

bufLen:缓冲区的长度。

nVMs:指向整数的指针。可以是NULL值。

返回:

成功时返回JNI_OK;失败时返回适当的JNI错误代码(负数)。


JNI_CreateJavaVM

jint JNI_CreateJavaVM(JavaVM **p_vm, void **p_env, void *vm_args);

加载并初始化Java VM。当前线程将附加到Java VM并成为主线程。将p_env参数设置为主线程的JNI接口指针。

不支持在单个进程中创建多个VM。

JNI_CreateJavaVM的第二个参数始终是指向JNIEnv *的指针,而第三个参数是指向JavaVMInitArgs结构的指针,该结构使用选项字符串来编码任意的VM启动选项:

typedef struct JavaVMInitArgs {
    jint version;

    jint nOptions;
    JavaVMOption *options;
    jboolean ignoreUnrecognized;
} JavaVMInitArgs;

options字段是以下类型的数组:

typedef struct JavaVMOption {
    char *optionString;  /* 以默认平台编码的字符串形式表示的选项 */
    void *extraInfo;
} JavaVMOption;

数组的大小由JavaVMInitArgs中的nOptions字段表示。如果ignoreUnrecognizedJNI_TRUEJNI_CreateJavaVM将忽略所有以"-X"或"_"开头的未识别选项字符串。如果ignoreUnrecognizedJNI_FALSEJNI_CreateJavaVM在遇到任何未识别选项字符串时立即返回JNI_ERR。所有Java VM必须识别以下一组标准选项:

标准选项
optionString 含义
-D<name>=<value> 设置系统属性
-verbose[:class|gc|jni] 启用详细输出。选项后面可以跟一个逗号分隔的名称列表,指示VM将打印哪种类型的消息。例如,"-verbose:gc,class"指示VM打印GC和类加载相关的消息。标准名称包括:gcclassjni。所有非标准(特定于VM的)名称必须以"X"开头。
vfprintf extraInfo是指向vfprintf挂钩的指针。
exit extraInfo是指向exit挂钩的指针。
abort extraInfo是指向abort挂钩的指针。

模块相关选项--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"

此外,每个VM实现可能支持自己的一组非标准选项字符串。非标准选项名称必须以"-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"; /* 用户类 */
options[1].optionString = "-Djava.library.path=c:\mylibs";  /* 设置本地库路径 */
options[2].optionString = "-verbose:jni";                   /* 打印JNI相关消息 */

vm_args.version = JNI_VERSION_1_2;
vm_args.options = options;
vm_args.nOptions = 3;
vm_args.ignoreUnrecognized = TRUE;

/* 请注意,在JDK/JRE中,不再需要调用
 * JNI_GetDefaultJavaVMInitArgs。
 */
res = JNI_CreateJavaVM(&vm, (void **)&env, &vm_args);
if (res < 0) ...

链接:

从实现Java虚拟机的本地库中导出。

参数:

p_vm:指向将放置结果VM结构的位置的指针。不能为NULL

p_env:指向将放置主线程的JNI接口指针的位置的指针。不能为NULL

vm_args:Java VM初始化参数。不能为NULL

返回:

成功时返回JNI_OK;失败时返回适当的JNI错误代码(负数)。


DestroyJavaVM

jint DestroyJavaVM(JavaVM *vm);

终止Java VM的操作,尽最大努力释放VM资源。

任何线程,无论是否已附加,都可以调用此函数。如果当前线程未附加,则首先将其附加。如果当前线程已附加,则如果其调用堆栈上有任何Java方法,则出现错误。

此函数等待直到所有非守护线程终止,不包括当前线程(如果它是非守护线程),然后启动关闭序列(请参见java.lang.Runtime)。当关闭序列完成时,Java虚拟机终止,导致仍在执行Java代码的任何线程停止执行该代码,并释放其能够释放的任何关联的VM资源。此时,当前线程不再附加到Java虚拟机,此函数返回给其调用者。

请注意,当Java虚拟机终止时,仍在执行本机代码的任何线程将继续执行该代码;但是,如果它们尝试恢复执行Java代码,则将停止执行。这包括守护线程和在关闭序列启动后启动的任何非守护线程。术语守护线程和非守护线程仅在附加的本机线程方面具有意义;未附加的本机线程不受Java虚拟机终止的影响。

链接:

JavaVM接口函数表中的索引3。

参数:

vm:将被销毁的Java虚拟机。它不能为NULL

返回:

成功时返回JNI_OK;失败时返回适当的JNI错误代码(负数)。


AttachCurrentThread

jint AttachCurrentThread(JavaVM *vm, void **p_env, void *thr_args);

将当前线程附加到Java虚拟机作为非守护线程。在p_env参数中返回一个JNI接口指针。

尝试附加已经附加的线程只会在p_env参数中返回其现有的JNI接口指针。已经附加的线程的守护状态通过调用此方法不会改变。

一个本机线程不能同时附加到两个Java虚拟机。

当线程附加到VM时,上下文类加载器是引导加载器。

链接:

JavaVM接口函数表中的索引4。

参数:

vm:当前线程将附加到的VM,不能为NULL

p_env:指向当前线程的JNI接口指针将被放置的位置。它不能为NULL

thr_args:可以为NULL或指向JavaVMAttachArgs结构的指针以指定附加信息:

    typedef struct JavaVMAttachArgs {
        jint version;
        char *name;    /* 线程的名称作为修改后的UTF-8字符串,或为NULL */
        jobject group; /* ThreadGroup对象的全局引用,或为NULL */
    } JavaVMAttachArgs

返回:

成功时返回JNI_OK;失败时返回适当的JNI错误代码(负数)。


AttachCurrentThreadAsDaemon

jint AttachCurrentThreadAsDaemon(JavaVM *vm, void **p_env, void *thr_args);

将当前线程附加到Java虚拟机作为守护线程。在p_env参数中返回一个JNI接口指针。

尝试附加已经附加的线程只会在p_env参数中返回其现有的JNI接口指针。已经附加的线程的守护状态通过调用此方法不会改变。

一个本机线程不能同时附加到两个Java虚拟机。

当线程附加到VM时,上下文类加载器是引导加载器。

链接:

JavaVM接口函数表中的索引7。

参数:

vm:当前线程将附加到的虚拟机实例。它不能为NULL

p_env:指向当前线程的JNIEnv接口指针将被放置的位置。它不能为NULL

thr_args:可以为NULL或指向JavaVMAttachArgs结构的指针以指定附加信息:

    typedef struct JavaVMAttachArgs {
        jint version;
        char *name;    /* 线程的名称作为修改后的UTF-8字符串,或为NULL */
        jobject group; /* ThreadGroup对象的全局引用,或为NULL */
    } JavaVMAttachArgs

返回

成功时返回JNI_OK;失败时返回适当的JNI错误代码(负数)。


DetachCurrentThread

jint DetachCurrentThread(JavaVM *vm);

将当前线程从Java虚拟机中分离。如果调用堆栈上有Java方法,则线程无法分离自身。

此线程仍持有的任何Java监视器都将被释放(尽管在正确编写的程序中,所有监视器在此时之前都已被释放)。线程现在被视为已终止,不再存活;所有等待此线程死亡的Java线程都会收到通知。

主线程可以从VM中分离。

尝试分离未附加的线程是一个空操作。

如果在调用DetachCurrentThread时存在异常,VM可能会选择报告其存在。

链接:

JavaVM接口函数表中的索引5。

参数:

vm:当前线程将被分离的VM。它不能为NULL

返回:

成功时返回JNI_OK;失败时返回适当的JNI错误代码(负数)。


GetEnv

jint GetEnv(JavaVM *vm, void **p_env, jint version);

链接:

JavaVM接口函数表中的索引6。

参数:

vm:要从中检索接口的虚拟机实例。它不能为NULL

p_env:指向当前线程的JNI接口指针将被放置的位置。它不能为NULL

version:请求的JNI版本。

返回:

如果当前线程未附加到VM,则将*env设置为NULL,并返回JNI_EDETACHED。如果指定的版本不受支持,则将*env设置为NULL,并返回JNI_EVERSION。否则,将*env设置为适当的接口,并返回JNI_OK