原生镜像兼容性指南
原生镜像使用与传统 Java 虚拟机 (VM) 不同的方法来编译 Java 应用程序。它区分构建时和运行时。在镜像构建时,native-image
构建器执行静态分析以查找从应用程序的入口点可达的所有方法。然后,构建器将这些方法(仅这些方法)编译成可执行二进制文件。由于这种不同的编译模型,Java 应用程序在编译成原生镜像时可能会表现出一些不同的行为。
原生镜像提供了一种优化方法,可以减少应用程序的内存占用和启动时间。这种方法依赖于“封闭世界假设”,在这种假设中,所有代码在构建时都是已知的。也就是说,运行时不会加载任何新代码。与大多数优化一样,并非所有应用程序都适合这种方法。如果native-image
构建器无法在构建时优化应用程序,它将生成一个所谓的“回退文件”,该文件需要 Java VM 才能运行。我们建议您查看原生镜像基础,以详细了解您的 Java 应用程序在构建时和运行时会发生什么。
需要元数据的功能 #
为了适合封闭世界假设,以下 Java 功能通常需要元数据才能在构建时传递给native-image
。此元数据可确保原生镜像使用最少的必要空间。
最近,通过在 GitHub 上发布共享可达性元数据,增强了原生镜像与最流行的 Java 库的兼容性。用户可以共享维护第三方依赖项元数据的负担,并重复使用它。请参阅可达性元数据,了解更多信息。
与封闭世界假设不兼容的功能 #
某些 Java 功能在封闭世界假设中尚未得到支持,如果使用,会导致回退文件。
invokedynamic
字节码和方法句柄 #
在封闭世界假设下,必须知道所有被调用的方法及其调用位置。invokedynamic
方法和方法句柄可以在运行时引入调用或更改被调用的方法。
请注意,javac
为 Java lambda 表达式和字符串连接生成的invokedynamic
用例是受支持的,因为它们不会在运行时更改被调用的方法。
在原生镜像中可能以不同方式运行的功能 #
原生镜像以与 Java VM 不同的方式实现某些 Java 功能。
安全管理器 #
即使在启动时通过-Djava.security.manager
设置了安全管理器,java.lang.System#getSecurityManager()
也会始终返回null
。
如果在程序启动时将-Djava.security.manager
设置为除disallow
之外的任何值,则使用非空参数调用java.lang.System#setSecurityManager(SecurityManager)
将抛出java.lang.SecurityException
。
信号处理程序 #
注册信号处理程序需要启动一个新线程来处理信号并调用关闭挂钩。默认情况下,在构建原生镜像时不会注册任何信号处理程序,除非用户显式注册。例如,不建议在构建共享库时注册默认信号处理程序,但对于在容器化环境(如 Docker 容器)中构建原生可执行文件,则需要包含信号处理程序。
要注册默认信号处理程序,请将--install-exit-handlers
选项传递给native-image
构建器。此选项为您提供与 Java VM 相同的信号处理程序。
类初始化器 #
默认情况下,类在运行时初始化。这可以确保兼容性,但会限制一些优化。为了加快启动速度并提高峰值性能,最好在构建时初始化类。可以使用选项--initialize-at-build-time
或--initialize-at-run-time
为特定类和包或所有类指定类初始化行为。默认情况下,JDK 类库的成员类将被初始化。
注意:在构建时初始化类可能会破坏现有代码中的特定假设。例如,在类初始化器中加载的文件可能在构建时与运行时不在同一个位置。此外,某些对象(如文件描述符或正在运行的线程)不得存储在原生可执行文件中。如果在构建时可以访问此类对象,则native image
构建器将失败并显示错误。
有关更多信息,请参阅原生镜像中的类初始化。
终结器 #
Java 基类java.lang.Object
定义了方法finalize()
。当垃圾收集器确定不再有对对象的引用时,它会在对象上调用此方法。子类可以覆盖finalize()
方法来释放系统资源或执行其他清理操作。
从 Java SE 9 开始,终结器已被弃用。它们难以实现,并且具有设计不当的语义。例如,终结器可能会通过在静态字段中存储对它的引用而导致对象再次可达。因此,终结器不会被调用。我们建议您将终结器替换为弱引用和引用队列。
线程 #
原生镜像没有实现java.lang.Thread
中已过时的长期方法,例如Thread.stop()
。
不安全的内存访问 #
如果在构建时初始化类,则使用sun.misc.Unsafe
访问的字段需要为静态分析标记为这种方式。在大多数情况下,这会自动发生:存储在static final
字段中的字段偏移量将自动从托管值(native image
构建器运行的 Java VM 的字段偏移量)重写为原生可执行文件的偏移量,并且作为重写的一部分,该字段将被标记为Unsafe
访问。对于非标准模式,可以使用注释RecomputeFieldValue
手动重新计算字段偏移量。
调试和监控 #
Java 有一些可选规范,Java 实现可以使用这些规范来调试和监控 Java 程序,包括 JVMTI。它们可以帮助您在运行时监控 Java VM 以获取事件,例如编译,例如,这些事件在大多数原生镜像中不会发生。这些接口建立在 Java 字节码在运行时可用的假设之上,而对于使用封闭世界优化构建的原生镜像而言,情况并非如此。由于native-image
构建器会生成原生可执行文件,因此用户必须使用原生调试器和监控工具(如 GDB 或 VTune),而不是面向 Java 的工具。JVMTI 和其他基于字节码的工具不受原生镜像支持。
Linux AArch64 架构的限制
除了下面描述的限制之外,大多数原生镜像功能都在 Linux AArch64 架构上受支持。
-R:[+|-]WriteableCodeCache
:必须禁用。--libc=<value>
:不支持musl
。--gc=<value>
:不支持 G1 垃圾收集器 (G1
)。
请此处查找native-image
构建器的选项列表。