平时我们运行 Java 服务,通常是:
bash
java -jar app.jar
但使用 GraalVM Native Image 后,启动命令可能变成:
bash
./app
这里最容易产生三个误解:
- Java 已经不需要任何运行时了;
- OpenJDK 本身包含 Substrate VM;
- 一个应用可以把部分代码编译成本地机器码,其余代码自动交给原来的 JAR 和 JVM。
这三个理解都不准确。Native Image 的本质是:在构建期分析 Java 应用,把可达代码提前编译成机器码,并把需要的 Java 运行时组件一起链接进最终可执行文件。
先给结论
普通 Java 应用走的是:
JAR → HotSpot JVM → 解释执行与 JIT → 机器码
Native Image 走的是:
JAR/字节码 → 静态分析与 AOT → 本地可执行文件
Native Image 通常能带来更快的启动速度和更低的常驻内存,但这不是所有业务下都成立的性能承诺。代价是构建更慢、动态能力受限,而且长时间运行时的峰值吞吐量未必超过 HotSpot JVM。
1. 普通 JAR 为什么需要 JVM?
JAR 本质上是一个归档文件,里面主要是 Java 字节码、资源和依赖信息。CPU 并不能直接执行 Java 字节码,所以 java -jar 需要先启动 JVM。

JVM 路线会经历类加载、字节码验证、解释执行和 JIT 编译。服务运行一段时间后,JIT 可以根据真实流量继续优化热点路径,所以长时间运行的服务往往能获得很好的峰值性能。
代价是:达到稳定峰值性能通常需要预热,JVM 本身还要维护类元数据、JIT 代码缓存等运行时结构。
2. Native Image 到底做了什么?
可以把一次 Native Image 构建简单理解为三步。
第一步:从入口向下找"会用到的代码"
构建器通常从 main 方法等入口开始,分析哪些类、方法和字段可能被调用,这些内容叫"可达代码"。不可达内容通常不会进入最终产物。
反射、JNI、资源文件、序列化和动态代理比较特殊:它们依赖运行时信息,仅靠普通静态分析不一定能发现,所以还需要框架生成或开发者提供 reachability metadata。
第二步:提前编译成机器码
普通 JVM 在运行时对热点代码做 JIT 编译;Native Image 则在构建时进行 AOT(Ahead-of-Time)编译。
因此最终产物已经包含当前操作系统和 CPU 架构能执行的机器码。Linux 上通常是 ELF 文件,macOS 上是 Mach-O,Windows 上则是 PE/.exe。
第三步:把必要运行时一起链接进去
Java 对象仍然需要堆,堆仍然需要 GC;线程、锁、异常和对象模型也不会凭空消失。Native Image 会把这些运行时组件与应用机器码一起链接进最终二进制。
因此,app 不是"只有业务代码的裸机器码",而是一个包含以下内容的本地程序:
- AOT 编译后的应用代码;
- 被分析为可达的 Java 类库代码;
- GC、线程、异常等运行时组件;
- 部分在构建期生成并写入 image heap 的对象。
这里的 app 只是示例文件名,并不是某个固定产品。
3. JDK、OpenJDK、GraalVM、Native Image、Substrate VM 是什么关系?

| 名称 | 简单理解 | 关键点 |
|---|---|---|
| JDK | Java 开发工具包 | 包含编译器、工具、类库和 JVM 等 |
| OpenJDK | Java SE 的开源实现项目与主线代码 | 多种 JDK 发行版以它为基础 |
| GraalVM JDK | 一种 JDK 发行版及相关编译技术集合 | 既能按 JVM 模式运行 JAR,也能提供 Native Image |
| Native Image | AOT 构建器及相关技术 | 输入字节码,输出平台相关二进制 |
| Substrate VM | Native Image 技术及其运行时实现基础 | 提供 GC、线程、异常、对象模型等能力,不是一个单独启动的 JVM 产品 |
需要特别注意两点:
- OpenJDK 主线本身不包含 Substrate VM。 某个基于 OpenJDK 的厂商发行版,可以额外打包 Native Image 能力,但不能因此说"所有 OpenJDK 都自带 Substrate VM"。
- 使用 GraalVM JDK 不等于使用 Native Image。 用 GraalVM JDK 执行
java -jar,仍然是在 JVM 模式下运行。
4. 为什么最终可以直接执行 ./app?
最直接的构建方式是:
bash
native-image -jar app.jar app
./app
第一条命令在构建阶段生成当前平台的可执行文件;第二条命令由操作系统直接加载这个文件。
运行 ./app 时不需要再安装外部 OpenJDK 或启动 HotSpot,因为需要的 Native Image 运行时组件已经进入产物。但是,这不代表它与操作系统完全无关:默认生成的通常是动态链接程序,仍可能依赖目标系统的 libc、证书、时区数据或其他本地库。
还有一个关键边界:native executable 不会把某些方法交给机器码、再把剩余方法自动回退给原 JAR/JVM。 缺少动态元数据时,通常表现为构建失败或运行失败。如果企业希望保留 JVM 回退路径,需要同时发布 JAR,并由启动脚本或编排系统显式选择,两者是两套启动路径。
5. Spring Boot 和 Quarkus 怎么构建?
下面命令来自当前官方构建方式,但能否直接运行取决于项目使用的框架版本、插件和 profile。真实项目应锁定 JDK、GraalVM、插件与框架版本。
Spring Boot
项目使用 spring-boot-starter-parent 提供的 native profile,并配置 GraalVM Native Build Tools 后,可以直接生成本地可执行文件:
bash
./mvnw -Pnative native:compile
./target/myproject
这里的 myproject 是示例产物名,真实名称由项目配置决定。若希望通过 Cloud Native Buildpacks 直接生成包含 native executable 的容器镜像,可执行:
bash
./mvnw -Pnative spring-boot:build-image
Gradle 项目在应用对应插件后,可使用 ./gradlew nativeCompile。Spring Boot 的 AOT 引擎会提前生成一部分代码和提示信息,但第三方 starter 仍可能需要额外的 Runtime Hints。
Quarkus
本机已正确配置 GraalVM 或 Mandrel 时,可以执行:
bash
./mvnw install -Dnative
./target/demo-1.0.0-SNAPSHOT-runner
第二行只是示例名称,真实格式通常由 artifactId 和 version 组成。
如果本机是 macOS、但目标产物要运行在 Linux 容器中,Quarkus 官方提供容器构建方式:
bash
./mvnw install -Dnative -DskipTests \
-Dquarkus.native.container-build=true
容器构建得到的是 Linux 可执行文件,不能直接拿回 macOS 运行。构建镜像、运行镜像、libc 和 CPU 架构都要匹配。
6. Docker 是必需的吗?
不是。Native Image 和 Docker 是两个不同层次:
- Native Image 负责把 Java 应用构建成本地可执行文件;
- Docker/OCI 镜像负责把程序、系统依赖和配置打包交付。
下面是一个简化的多阶段构建示例。版本标签用于说明"应固定版本",上线前仍要按项目版本矩阵验证:
dockerfile
FROM ghcr.io/graalvm/native-image-community:25i2 AS build
WORKDIR /workspace
COPY app.jar .
RUN native-image -jar app.jar app
FROM oraclelinux:9-slim
COPY --from=build /workspace/app /app
ENTRYPOINT ["/app"]
Native Image 默认构建动态链接二进制。Linux 下可以使用 --static --libc=musl 构建全静态产物,也可以使用 --static-nolibc 构建 mostly-static 产物,但这会引入不同的 libc 和依赖约束,不能简单理解为"加一个参数就一定能用 scratch 镜像"。

7. 谁来管理内存和 GC?
Native Image 程序仍然是操作系统中的普通进程,只是它不再运行在传统 HotSpot JVM 中。
职责可以这样分:
- 操作系统:管理进程虚拟内存、页映射、底层内存分配和原生线程调度;
- Native Image 运行时:维护 Java 对象模型和 Java heap,并由 GC 判断对象是否可达、回收不再使用的对象;
- 应用程序:决定对象创建速度、线程数量、直接内存使用以及缓存规模。
所以 -Xmx 只是在约束 Java heap,不等于整个进程 RSS,更不等于容器的总内存。线程栈、直接内存、映射进来的二进制和共享库、运行时结构都要另外计算。
构建阶段和运行阶段也不要混淆:构建器本身运行在 JVM 上,-J-Xmx 调整的是构建器 JVM 的堆;产物启动时使用的 -Xmx,调整的才是 native 程序的 Java heap。
8. 反射、动态代理为什么容易出问题?
HotSpot JVM 运行时通常保留完整 classpath,可以在运行中继续加载类和资源。Native Image 为了缩小产物,会在构建期决定哪些内容进入二进制,这就是闭世界假设。
以下写法对静态分析不友好:
java
String className = readFromConfig();
Class<?> type = Class.forName(className);
构建器无法只看代码就知道配置文件最终会返回哪个类名。因此,反射、动态代理、JNI、序列化、资源文件和运行时类加载,可能需要 reachability metadata、Spring Runtime Hints 或 Quarkus extension metadata。
正确做法通常是:
- 优先使用框架和依赖官方提供的 Native Image 支持;
- 只为确实使用的动态内容补元数据;
- 在最终 native executable 上运行集成测试,而不只跑 JVM 单测。
9. 它是不是很像 Go 和 Rust?
从部署形态看,确实很像:最终都是一个可以直接执行的本地程序。但内部机制并不一样。
| 方案 | 主要特点 | 运行时与内存 |
|---|---|---|
| Java + HotSpot | 运行时 JIT,可根据真实流量优化 | 完整 JVM 与 GC |
| Java + Native Image | 闭世界分析与 AOT,保留 Java 生态 | 二进制内含所需 Native Image 运行时与 GC |
| Go | 原生编译,语言和工具链强调简单部署 | 二进制通常带 Go runtime 与 GC |
| Rust | 原生编译,以所有权系统管理大部分内存安全 | 通常没有 Java/Go 式 GC |
因此,"都能运行 ./app"只说明交付形式接近,并不代表编译器、内存模型和运行时相同。Substrate VM 也不是 Linux 的能力,更不是 Go、Rust 使用的公共 VM。
10. 企业项目应该怎么选?
适合优先试点 Native Image 的场景:
- CLI 工具、短生命周期任务;
- FaaS、Serverless、scale-to-zero;
- 冷启动是明确瓶颈的服务;
- 内存预算紧张、实例密度很高的容器环境。
更适合继续使用 JVM 的场景:
- 长时间运行并追求峰值吞吐量;
- 大量依赖运行时反射、脚本、插件或动态类加载;
- 已有成熟的 JVM 诊断、调优和发布体系;
- Native Image 不能带来可量化业务收益。
企业选型不要只比较"能否启动",至少要测量:
- 冷启动与首个请求耗时;
- 稳态吞吐量与 P95/P99 延迟;
- Java heap、进程 RSS 和容器总内存;
- 构建时间、构建机资源与跨架构成本;
- 故障诊断、升级、回滚和团队维护成本。
对于已有大型 Spring Boot 核心系统,更稳妥的方式通常不是全量迁移,而是先挑一个边界清晰的 CLI、诊断工具或轻量服务做双产物试点:同一份代码同时构建 JAR 与 native executable,再用真实流量和总成本决定是否推广。
大厂都在使用吗?
已经有公开的生产案例,但不能理解为"大厂正在全面放弃 JVM"。例如,GraalVM 官方案例页记录了阿里巴巴将部分 SOFABoot 微服务编译成 ELF 可执行文件,也列出了 Disney Streaming 的 Serverless 冷启动案例和 Oracle 的 Native Image 应用。这只能证明 Native Image 已能用于真实生产场景,不能替代你对自己业务的压测和兼容性验证。
11. 最后总结
Native Image 并不是"Java 突然不需要运行时了",而是把大量运行时工作提前到了构建期。
可以记住下面四句话:
- JAR 需要 JVM 将字节码变成 CPU 能执行的机器码。
- Native Image 在构建期完成静态分析和 AOT,并生成平台相关可执行文件。
- GC、线程、异常等运行时组件会进入最终产物,所以
./app可以独立于外部 HotSpot/JDK 启动。 - 启动快、内存低是常见优势,不是无条件结论;真正选型要看动态兼容性、稳态性能和总成本。
参考资料(官方)
- GraalVM Native Image Basics
- GraalVM Native Image Build Overview
- GraalVM Reachability Metadata
- GraalVM:构建静态或 mostly-static 可执行文件
- GraalVM Native Image:内存占用与 GC
- GraalVM Community Edition 容器镜像
- Spring Boot:构建 Native Image 应用
- Spring Boot:Native Image 介绍与限制
- Quarkus:Building a Native Executable
- GraalVM 官方采用案例
- OpenJDK 项目
文中命令用于解释构建链路。正式项目请固定并验证 JDK、GraalVM、框架、插件、操作系统、libc 和 CPU 架构版本。