GraalVM Native Image 基础:构建期与运行期、镜像堆与静态分析原理详解
2026/9/20 12:59:45 网站建设 项目流程

GraalVM Native Image 基础:构建期与运行期、镜像堆与静态分析原理详解

【免费下载链接】graalGraalVM compiles applications into native executables that start instantly, scale fast, and use fewer compute resources 🚀项目地址: https://gitcode.com/gh_mirrors/gr/graal

本文以 GraalVM Native Image 的核心机制为主线,系统讲解native-image构建器如何把 Java 字节码加工成独立的原生可执行文件(或共享库),重点厘清三个最基础也最关键的概念——构建期(Build Time)与运行期(Run Time)镜像堆(Native Image Heap)静态分析(Static Analysis)与闭世界假设(Closed-World Assumption)。读完本文,你将能够看懂native-image构建日志中每一阶段的含义,正确使用--initialize-at-build-time/--initialize-at-run-time等类初始化选项,并理解为什么反射、序列化等动态特性需要额外的可达性元数据(Reachability Metadata)。

从字节码到原生二进制的三步走

Native Image 本身是用 Java 编写的,它以Java 字节码(Java bytecode)作为输入,产出一个独立的二进制产物——可以是可执行文件(executable),也可以是共享库(shared library)。整个产出过程包含三个关键环节:

  1. 构建期间执行用户代码:在产出二进制的过程中,Native Image 可以在宿主 JVM 上运行一部分用户代码(典型场景就是类初始化)。
  2. 链接:把编译后的用户代码、Java 运行时的一部分(例如垃圾回收器、线程支持等)以及代码执行产生的结果一起链接进二进制。
  3. 产出:最终得到被称作native executable(原生可执行文件)native image(原生镜像)的产物。

与之对应,产出该二进制的工具被称为native-imagebuilder(native-image构建器)native-imagegenerator(native-image生成器)。为了严格区分"在构建期间执行的代码"与"在运行期间执行的代码",Native Image 引入了本文的核心概念之一:构建期与运行期

native-image命令的三种典型用法(详见仓库中的 Native Image Build Overview)为:

# 从当前目录下的主类构建 native-image [options] <mainclass> [imagename] [options] # 从 JAR 文件构建 native-image [options] -jar jarfile [imagename] [options] # 从 Java 模块构建 native-image [options] -m <module>/<mainClass> [imagename] [options]

选项从左到右依次求值;类路径通过-cp <classpath>指定(Linux/macOS 上以冒号分隔,Windows 上以分号分隔)。

构建期(Build Time)与运行期(Run Time)

两个阶段如何划分

  • 构建期(build time):指镜像构建过程中,在宿主 JVM 上执行的代码。这段代码可以产生副作用,例如向某个类的静态字段写入值。这些被写入的值会被保存进镜像堆中。
  • 运行期(run time):指二进制产物(镜像)被实际执行时对应的代码与状态。

理解这两个概念最直观的入口是可配置的类初始化(configurable class initialization)。在 Java 语义中,一个类在首次被使用时才会被初始化;而在 Native Image 中:

  • 每个在构建期被使用过的 Java 类,都被称为构建期初始化(build-time initialized)
  • 需要注意:仅仅**加载(load)**一个类并不一定会初始化它。
  • 构建期初始化类的静态初始化器(static initializer),实际运行在执行镜像构建的 JVM 上
  • 如果一个类在构建期被初始化,它的静态字段会被保存进最终产出的二进制中;在运行期,即使第一次使用这个类,也不会再次触发类初始化

用户在两种典型场景下会触发构建期类初始化:

  • native-image构建器传递--initialize-at-build-time=<class>参数;
  • 在一个构建期初始化的类的静态初始化器中使用了另一个类。

Native Image 默认会在镜像构建期初始化大量高频使用的 JDK 类(例如java.lang.Stringjava.util.**等)。需要强调的是,构建期类初始化是一个专家级特性,并非所有类都适合在构建期初始化——错误的配置可能导致运行期行为异常,甚至把敏感数据(如密码、密钥)固化进二进制。

用 HelloWorld 演示构建期与运行期的差别

原文档给出了如下示例(NativeImageBasics.md),我们将它完整展开。把代码保存为_HelloWorld.java_

public class HelloWorld { static class Greeter { static { System.out.println("Greeter is getting ready!"); } public static void greet() { System.out.println("Hello, World!"); } } public static void main(String[] args) { Greeter.greet(); } }

先在 JVM 上编译并运行,观察类初始化发生的时机:

javac HelloWorld.java java HelloWorld Greeter is getting ready! Hello, World!

接着用默认配置构建原生镜像并执行:

native-image HelloWorld ======================================================================================================================== GraalVM Native Image: Generating 'helloworld' (executable)... ======================================================================================================================== ... Finished generating 'helloworld' in 14.9s.
./helloworld Greeter is getting ready! Hello, World!

程序启动后调用Greeter.greet,触发了Greeter的类初始化,打印出Greeter is getting ready!。此时我们说Greeter的类初始化器在镜像运行期执行

现在换一种方式:显式要求native-image在构建期初始化Greeter

native-image HelloWorld --initialize-at-build-time=HelloWorld\$Greeter ======================================================================================================================== GraalVM Native Image: Generating 'helloworld' (executable)... ======================================================================================================================== Greeter is getting ready! [1/8] Initializing... (3.1s @ 0.15GB) Java version: 24+36, vendor version: Oracle GraalVM 24+36.1 Graal compiler: optimization level: 2, target machine: armv8.1-a, PGO: ML-inferred C compiler: cc (apple, arm64, 16.0.0) Garbage collector: Serial GC (max heap size: 80% of RAM) ... Finished generating 'helloworld' in 13.6s. ./helloworld Hello, World!

注意输出顺序的变化:

  • Greeter is getting ready!出现在镜像构建期间(紧跟构建器横幅之后、进入[1/8] Initializing...之前)——Greeter的类初始化器在镜像构建期执行
  • 运行./helloworld时只剩Hello, World!,因为运行期HelloWorld调用Greeter.greet时,Greeter已经处于已初始化状态
  • 构建期间初始化过的类的静态字段,会被存储进镜像堆。

从源码看类初始化选项的实现

上述行为在仓库源码中有明确的实现对应。类初始化的核心选项定义在 ClassInitializationOptions.java:

  • --initialize-at-build-time:接收一个逗号分隔的包名/类名列表(以及隐式的所有父类),在镜像生成期间初始化;空字符串表示所有包
  • --initialize-at-run-time:接收一个逗号分隔的包名/类名列表(以及隐式的所有子类),必须在运行期初始化(空字符串当前不支持)。
  • -H:+PrintClassInitialization(对应PrintClassInitialization选项):打印分析检测到的所有类的类初始化信息,用于排查"哪些类为什么被初始化"。

选项的解析与校验逻辑位于 ClassInitializationFeature.java:构建器会逐个拆解逗号分隔的值,根据后缀判定是构建期初始化还是运行期初始化,并最终调用initializeAtBuildTime/initializeAtRunTime注册到初始化支持类中。此外,com.oracle.svmcom.oracle.graal等 Native Image 自身的包总是被强制在构建期初始化(NATIVE_IMAGE_CLASS_REASON)。

关于类初始化策略更完整的讨论(包括"安全类自动构建期初始化"、-H:+PrintClassInitialization的使用等),请继续阅读仓库文档 Class Initialization in Native Image。

镜像堆(Native Image Heap)

镜像堆里有什么

Native Image 堆(也叫image heap,镜像堆)包含三类内容:

  • 镜像构建期间创建、且从应用程序代码可达的对象(典型来源就是构建期类的静态字段所引用的对象);
  • 原生镜像中使用的所有java.lang.Class对象的对应体(在 Native Image 运行时由DynamicHub表示,参见 DynamicHub.java);
  • 内嵌在方法代码中的对象常量(object constants)——详见 ReachabilityMetadata。

当原生镜像启动时,它会把初始镜像堆从二进制中整体拷贝出来,作为运行期堆的起点。这正是"把工作从运行期挪到构建期"能够显著加速启动的根本原因:大量对象在构建期就已经被创建、序列化进二进制,运行期只需按地址拷贝即可。

用 System Property 演示镜像堆

原文档给出了一个非常经典的示例,我们完整保留。把代码保存为_Example.java_

class Example { private static final String message; static { message = System.getProperty("message"); } public static void main(String[] args) { System.out.println("Hello, World! My message is: " + message); } }

先在 JVM 上编译并运行,注意-Dmessage的值随命令行变化:

javac Example.java java -Dmessage=hi Example Hello, World! My message is: hi
java -Dmessage=hello Example Hello, World! My message is: hello
java Example Hello, World! My message is: null

现在构建一个把Example放在构建期初始化的原生镜像,并在构建时传入-Dmessage=native

native-image Example --initialize-at-build-time=Example -Dmessage=native ================================================================================ GraalVM Native Image: Generating 'example' (executable)... ================================================================================ ... Finished generating 'example' in 19.0s.
./example Hello, World! My message is: native
./example -Dmessage=aNewMessage Hello, World! My message is: native

关键结论:

  1. Example的类初始化器在镜像构建期执行,读取了构建期 JVM 的属性-Dmessage=native,为message字段创建了一个String对象;
  2. 这个String对象被存储进镜像堆并固化在二进制中;
  3. 运行期即使通过./example -Dmessage=aNewMessage传入了新属性,message依然是构建期固化的值native——因为类已经初始化过了,静态字段不会再重新计算。

这正是"构建期状态进入镜像堆"的直接体现:镜像堆是构建期执行结果的快照

从源码看镜像堆

镜像堆的构建与写入在构建器侧由多个类协作完成,其中核心之一是本仓库中的 NativeImageHeap.java:它负责收集、排序、编码镜像堆中的对象,最终由NativeImageHeapWriter等组件写入二进制。与之配合的还有ImageHeapCollectionFeatureImageHeapObjectAdderImageHeapConnectedComponentsFeature等实现类(位于 heap 与 image 目录),它们共同决定哪些对象进入镜像堆、以什么顺序布局。

一个实用的运行期排查手段:如果某个对象在运行期被访问时报错,提示其类型未被--initialize-at-build-time标记(见 ClassInitializationFeature.java 附近对 "objects ended up in the image heap without their type being marked" 的检测与告警),说明该对象的产生路径需要显式配置构建期初始化,或者改为在运行期创建。

静态分析(Static Analysis)

什么是可达代码

静态分析是决定一个应用程序到底使用了哪些程序元素(类、方法、字段)的过程,被判定为使用的元素称为可达代码(reachable code)。只有可达的元素才会被包含进最终镜像。

分析本身由两部分组成:

  1. 扫描方法的字节码:确定从这个方法还能触达哪些其他元素;
  2. 扫描镜像堆中的根对象(例如静态字段):确定从这些对象还能触达哪些类。

整个分析从应用的入口点(即main方法)出发,不断迭代扫描新发现的元素,直到某次扫描不再改变任何元素的可达性(达到不动点)为止。

迭代分析在源码中的体现

这一"迭代至不动点"的机制在仓库源码中有明确实现。分析引擎的主循环位于 AbstractAnalysisEngine.java 的runAnalysis方法中:循环内反复调用finish()执行一轮分析,直到analysisEndCondition返回真;如果迭代超过 1000 次仍未收敛,会抛出错误,提示"静态分析没有达到不动点"(正常情况通常少于 10 次迭代)。可执行的根方法与根对象的注册(如addRootMethod)则由 ReachabilityAnalysis.java 等类提供。

闭世界假设(Closed-World Assumption)

镜像构建完成之后,运行期不能再加入任何新的元素——例如通过类加载(class loading)动态添加类是不被允许的。这一约束被称为闭世界假设(closed-world assumption)

它是理解 Native Image 一切行为的总纲:

  • 因为世界在构建期"闭合",所以才能放心剔除不可达代码,做到极小的二进制体积;
  • 也正因为世界闭合,反射(reflection)、JNI、资源访问、序列化、动态代理等依赖运行期动态查找的 JVM 特性,必须通过**可达性元数据(Reachability Metadata)**在构建期显式声明,否则运行期会抛出缺失注册错误(如MissingReflectionRegistrationError)。相关内容可参阅 Reachability Metadata 文档,其中也解释了如何在代码中提供常量参数(如Class.forName("Foo"))让构建器在构建期自动求值并存入初始镜像堆——这恰好把本文的"构建期/运行期"与"镜像堆"两个概念联系了起来。

实战进阶:构建期与运行期类初始化的性能对比

为了更直观地体会构建期初始化带来的收益,仓库配套指南 Specify Class Initialization Explicitly 提供了一个完整的TalkParser演示:解析一段文本块并生成Talk记录列表。

import java.util.ArrayList; import java.util.List; import java.util.Scanner; public class TalkParser { private static final List<Talk> TALKS = new ArrayList<>(); static { Scanner s = new Scanner(""" Asynchronous Programming in Java: Options to Choose from by Venkat Subramaniam Anatomy of a Spring Boot App with Clean Architecture by Steve Pember Java in the Cloud with GraalVM by Alina Yurenko Bootiful Spring Boot 3 by Josh Long """); while (s.hasNextLine()) { TALKS.add(new Talk(s.nextLine())); } s.close(); } public static void main(String[] args) { System.out.println("Talks loaded using scanner:"); for (Talk talk : TALKS) { System.out.println("- " + talk.name()); } } } record Talk (String name) {}

编译后分别做两次构建:

javac TalkParser.java # 运行期初始化(默认行为,也可显式声明) native-image --initialize-at-run-time=TalkParser,Talk -o runtime-parser TalkParser # 构建期初始化 native-image --initialize-at-build-time=TalkParser,Talk -o buildtime-parser TalkParser

实测对比(不同机器结果不同,重点看趋势):

  • 运行期初始化版本:解析逻辑保留在二进制中,运行期才执行Scanner解析,产物体积约13M
  • 构建期初始化版本:文本块在构建期就被解析完,二进制中只固化解析得到的Talk记录,产物体积下降到约6.4M
  • perf stat观察指令数,构建期初始化版本从约 11.8M 条指令下降到约 8.6M 条,降幅接近 30%——因为大部分解析基础组件(Scanner相关)在静态分析时已不可达,被整体剔除。

该指南还给出了两条重要建议:--initialize-at-build-time尽量只针对单个类使用;错误的构建期初始化可能把敏感数据(如密码、加密密钥)固化进二进制,生产环境需格外谨慎。

小结与进一步阅读

本文梳理了 GraalVM Native Image 的三大基石:

  1. 构建期 vs 运行期:理解代码与状态在哪个阶段产生,是使用--initialize-at-build-time/--initialize-at-run-time的前提;
  2. 镜像堆:构建期结果被固化进二进制的载体,也是启动加速的来源;
  3. 静态分析与闭世界假设:决定"哪些代码进入镜像",也解释了为什么动态特性需要元数据。

在此基础上,建议继续阅读仓库中的以下文档以构建完整知识体系:

  • Native Image Build Overview:native-image命令语法、构建完成通知技巧;
  • Class Initialization in Native Image:类初始化的性能影响、"安全类"自动构建期初始化策略与约束;
  • Specify Class Initialization Explicitly:类初始化配置的完整实战演练;
  • Reachability Metadata:闭世界假设下反射、JNI、资源、序列化等动态特性的元数据配置参考。

【免费下载链接】graalGraalVM compiles applications into native executables that start instantly, scale fast, and use fewer compute resources 🚀项目地址: https://gitcode.com/gh_mirrors/gr/graal

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询