Skip to content
欢迎扫码关注公众号

JEP 472: Prepare to Restrict the Use of JNI | 准备限制 JNI 的使用

概述

对使用 Java 本地接口(JNI) 的情况发出警告,并调整 外部函数与内存(FFM)API,以一致的方式发出警告。所有这些警告旨在为开发者准备一个未来的版本,该版本通过统一限制 JNI 和 FFM API 来确保默认情况下的一致性。应用程序开发者可以通过仅在必要时选择性地启用这些接口来避免当前的警告和未来的限制。

目标

  • 保持 JNI 作为与本地代码互操作的标准方式的地位。

  • 准备 Java 生态系统迎接未来的版本,默认不允许通过 JNI 或 FFM API 与本地代码互操作。自该版本起,应用程序开发者必须在启动时显式启用 JNI 和 FFM API 的使用。

  • 统一 JNI 和 FFM API 的使用方式,使库维护者可以在两者之间迁移而无需应用程序开发者更改任何命令行选项。

非目标

  • 不是目标弃用 JNI 或将 JNI 从 Java 平台中移除。

  • 不是目标限制通过 JNI 调用的本地代码的行为。例如,所有的本地 JNI 函数 仍然可以由本地代码使用。

动机

Java 本地接口(JNI) 在 JDK 1.1 中被引入,成为 Java 代码和通常用 C 编写的本地代码之间进行互操作的主要手段。JNI 允许 Java 代码调用本地代码(称为 下调用),以及本地代码调用 Java 代码(称为 上调用)。

不幸的是,Java 代码与本地代码之间的任何交互都存在风险,因为它可能危及应用程序和 Java 平台本身的完整性。根据 默认一致性 策略,所有能够破坏完整性的 JDK 特性必须获得应用开发者明确的批准。

以下是四种常见的交互及其风险:

  1. 调用本地代码可能导致任意的 未定义行为,包括 JVM 崩溃。此类问题无法被 Java 运行时阻止,也不会引发 Java 代码可以捕获的异常。

  2. 本地代码和 Java 代码经常通过 直接字节缓冲区 交换数据,这是不由 JVM 垃圾收集器管理的内存区域。本地代码可能会生成一个由无效内存区域支持的字节缓冲区;在 Java 代码中使用这样的字节缓冲区几乎肯定会引起未定义行为。

  3. 本地代码可以使用 JNI 访问字段和调用方法,而不经过 JVM 的任何访问检查。本地代码甚至可以使用 JNI 改变 final 字段的值,即使它们已经被初始化很久了。因此,调用本地代码的 Java 代码可能会违反其他 Java 代码的一致性。

  4. 使用某些 JNI 函数不正确的本地代码,主要是 GetPrimitiveArrayCriticalGetStringCritical,可能会导致 不良的垃圾回收行为,这种行为可能在程序生命周期中的任何时间表现出来。

外部函数与内存(FFM)API 作为 JNI 的优选替代方案在 JDK 22 中引入,它同样面临 第一和第二种风险。在 FFM API 中,我们采取了一种主动的方法来缓解这些风险,将那些可能危及一致性的动作与不会这样做的动作分开。因此,FFM API 的一些部分被分类为 受限方法,这意味着应用开发者必须批准其使用并通过 java 启动器命令行选项选择加入。JNI 应该效仿 FFM API 的例子,朝着默认实现一致性的方向努力。

准备限制 JNI 的使用是长期协调工作的一部分,目的是确保 Java 平台具有 默认一致性。其他举措包括移除 sun.misc.Unsafe 中的内存访问方法(JEP 471)和限制动态加载代理(JEP 451)。这些努力将使 Java 平台更加安全和高效。它们还将减少应用程序开发者因旧版 JDK 上使用的库在新版本中不再支持 API 变化而导致无法升级的风险。

描述

在 JDK 22 及之后的版本中,您可以通过 Java 本地接口(JNI)或外部函数与内存(FFM)API 调用本地代码。在这两种情况下,您必须首先加载一个本地库并将 Java 构造链接到库中的某个函数。这些加载和链接步骤在 FFM API 中是 受限的,这意味着默认情况下它们会在运行时发出警告。在 JDK 24 中,我们将限制 JNI 中的加载和链接步骤,使它们也默认在运行时发出警告。

我们称对加载和链接本地库的限制为 本地访问限制。在 JDK 24 中,无论使用 JNI 还是 FFM API 来加载和链接本地库,都将统一应用本地访问限制。JNI 中现在受本地访问限制影响的确切操作,即加载和链接本地库的操作,将在 后面 描述。

我们将随着时间的推移加强本地访问限制的效果。与其发出警告,未来的 JDK 版本将在 Java 代码使用 JNI 或 FFM API 加载和链接本地库时,默认抛出异常。此举的目的不是为了阻止 JNI 或 FFM API 的使用,而是为了确保应用程序和 Java 平台具有 默认一致性

启用本地访问

应用程序开发者可以通过在启动时为选定的 Java 代码启用本地访问来避免警告(以及未来的异常)。启用本地访问确认了应用程序需要加载和链接本地库,并解除本地访问限制。

根据 默认一致性 策略,启用本地访问的是应用程序开发者(或可能是部署者,在应用程序开发者的建议下),而不是库开发者。依赖 JNI 或 FFM API 的库开发者应告知其用户需要使用以下方法之一启用本地访问。

要为类路径上的所有代码启用本地访问,请使用以下命令行选项:

bash
java --enable-native-access=ALL-UNNAMED ...

要为模块路径上的特定模块启用本地访问,请传递一个以逗号分隔的模块名称列表:

bash
java --enable-native-access=M1,M2,... ...

如果代码满足以下条件,则受本地访问限制的影响:

  • 它调用了 System::loadLibrarySystem::loadRuntime::loadLibraryRuntime::load,或者
  • 它声明了一个 native 方法。

仅调用不同模块中声明的 native 方法的代码不需要启用本地访问。

大多数应用程序开发者会直接将 --enable-native-access 传递给 java 启动器的一个启动脚本中,但也可以使用其他技术:

  • 可以通过设置环境变量 JDK_JAVA_OPTIONS 间接将 --enable-native-access 传递给启动器。

  • 可以将 --enable-native-access 放入传递给启动器的参数文件中,例如,java @config

  • 可以将 Enable-Native-Access: ALL-UNNAMED 添加到可执行 JAR 文件的清单中,即通过 java -jar 启动的 JAR 文件。(Enable-Native-Access 清单条目的唯一支持值是 ALL-UNNAMED;其他值会导致抛出异常。)

  • 如果为您的应用程序创建了一个自定义 Java 运行时,可以通过 jlink--add-options 选项传递 --enable-native-access 选项,以便为生成的运行时镜像启用本地访问。

  • 如果您的代码动态地创建模块,可以通过 ModuleLayer.Controller::enableNativeAccess 方法为它们启用本地访问,该方法本身是一个受限方法。代码可以通过 Module::isNativeAccessEnabled 方法动态检查其模块是否已启用本地访问。

  • JNI 调用 API 允许本地应用程序在其自己的进程中嵌入 JVM。使用 JNI 调用 API 的本地应用程序可以在 创建 JVM 时通过传递 --enable-native-access 选项为其嵌入式 JVM 中的模块启用本地访问。

更加选择性地启用本地访问

--enable-native-access=ALL-UNNAMED 选项较为粗糙:它为类路径上的所有类解除 JNI 和 FFM API 的本地访问限制。为了限制风险并实现更高的完整性,我们建议将使用 JNI 或 FFM API 的 JAR 文件移到模块路径上。这允许只为那些 JAR 文件专门启用本地访问,而不是整个类路径。无需对 JAR 文件进行模块化即可将其从类路径移动到模块路径;Java 运行时会将其视为基于其文件名的 自动模块

控制本地访问限制的效果

如果未为某个模块启用本地访问,则该模块中的代码执行受限操作将是非法的。Java 运行时在这种操作尝试时采取的动作由新的命令行选项 --illegal-native-access 控制,其形式和精神类似于 JDK 9 中 JEP 261 引入的 --illegal-access 选项。具体工作方式如下:

  • --illegal-native-access=allow 允许操作继续。

  • --illegal-native-access=warn 允许操作但在特定模块中第一次发生非法本地访问时发出警告。每个模块最多发出一次警告。

    此模式是 JDK 24 中的默认模式。它将在未来的版本中逐步淘汰,并最终被移除。

  • --illegal-native-access=deny 对每次非法本地访问操作抛出 IllegalCallerException 异常。

    此模式将成为未来版本的默认模式。

deny 成为默认模式时,allow 将被移除,但至少在一个发行版中仍会支持 warn

为了为未来做准备,我们建议您使用 deny 模式运行现有代码,以识别需要本地访问的代码。

与 FFM API 的对齐

在 JDK 24 之前,如果通过 --enable-native-access 选项为一个或多个模块启用了本地访问,则任何其他模块尝试调用 受限 FFM 方法 都会导致抛出 IllegalCallerException

为了使 FFM API 与 JNI 保持一致,我们将放宽这种行为,使得非法本地访问操作在 FFM API 中被完全按照 JNI 的方式处理。这意味着,在 JDK 24 中,此类操作将导致警告而不是异常。

您可以通过组合使用以下选项来恢复旧的行为:

bash
java --enable-native-access=M,... --illegal-native-access=deny ...

加载本地库时的警告

在 JNI 中,通过 java.lang.Runtime 类的 loadloadLibrary 方法加载本地库。(java.lang.System 类中同名的便捷方法 loadloadLibrary 只是调用了系统范围 Runtime 实例对应的相应方法。)

加载本地库是有风险的,因为它可能导致本地代码运行:

  • 如果本地库定义了 初始化函数,那么操作系统在加载库时会运行它们;这些函数包含任意的本地代码。

  • 如果本地库定义了一个 JNI_OnLoad 函数,那么 Java 运行时在加载库时会调用它;此函数也包含任意的本地代码。

由于存在风险,loadloadLibrary 方法在 JDK 24 中受到限制,就像 FFM API 中的 SymbolLookup::libraryLookup 方法一样受到限制。

当从没有启用本地访问的模块调用受限方法时,JVM 运行该方法,但默认情况下会发出一条标识调用者的警告:

WARNING: A restricted method in java.lang.System has been called
WARNING: System::load has been called by com.foo.Server in module com.foo (file:/path/to/com.foo.jar)
WARNING: Use --enable-native-access=com.foo to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

对于任何特定模块,最多发出一条这样的警告,并且仅在尚未为此模块发出警告的情况下才发出。警告写入标准错误流。

链接本地库时的警告

首次调用 native 方法时,它会自动链接到本地库中的 对应函数。这个称为 绑定 的链接步骤在 JDK 24 中是受限操作,就像在 FFM API 中 获取下行调用方法句柄 是受限操作一样。

当首次调用未启用本地访问的模块中声明的 native 方法时,JVM 绑定 native 方法,但默认情况下会发出一条标识调用者的警告:

WARNING: A native method in org.baz.services.Controller has been bound
WARNING: Controller::getData in module org.baz has been called by com.foo.Server in an unnamed module (file:/path/to/foo.jar)
WARNING: Use --enable-native-access=org.baz to avoid a warning for native methods declared in org.baz
WARNING: Native methods will be blocked in a future release unless native access is enabled

对于任何特定模块,最多发出一条这样的警告。具体来说:

  • 警告仅在 native 方法绑定时发出,即第一次调用 native 方法时发生。每次调用 native 方法都不会重复发出警告。

  • 警告仅在特定模块中声明的任何 native 方法首次绑定时发出,除非已经为此模块发出了警告。

警告写入标准错误流。

识别原生代码的使用

  • JFR 事件 jdk.NativeLibraryLoadjdk.NativeLibraryUnload 跟踪本地库的加载和卸载情况。

  • 为了帮助识别使用 JNI 的库,一个新的 JDK 工具(暂定名为 jnativescan)静态扫描提供的模块路径或类路径中的代码,并报告受限方法的使用情况以及 native 方法的声明情况。

未来工作

  • 为了促进可靠的配置,允许模块声明断言该模块需要本地访问,无论是通过 JNI 还是 FFM API。启动时,Java 运行时将拒绝加载任何需要本地访问但命令行上未启用本地访问的模块。

  • 为了允许使用 FFM API 但不允许使用 JNI,提供一个命令行选项,该选项允许前者而不允许后者。JNI 允许本地代码破坏 Java 代码的封装,这可能会以 FFM API 不具有的方式干扰未来的 JVM 优化。

风险和假设

  • JNI 自 JDK 1.1 以来一直是 Java 平台的一部分,因此存在现有应用程序将受到 JNI 使用限制影响的风险。对 Maven Central 上的工件进行分析发现,大约 7% 的现有工件依赖于本地代码。其中,约有 25% 直接使用 JNI;其余依赖于使用 JNI 的其他工件,直接或间接。

  • 我们假设那些直接或间接依赖于本地代码的应用程序的开发者能够配置 Java 运行时以启用 JNI 的使用,如 上文所述。这类似于他们已经可以配置 Java 运行时以通过 --add-opens 禁用模块的强封装。

替代方案

  • JVM 可以在本地代码使用 JNI 函数 访问 Java 字段和方法时应用访问控制规则,而不是限制本地库的加载和 native 方法的绑定。然而,这对于维护完整性是不够的,因为无论是否使用 JNI 函数,任何本地代码的使用都可能导致未定义行为。出于同样的原因,即使 FFM API 不提供从本地代码访问 Java 对象的功能,FFM API 的部分功能也是受限的。