Java 不再需要 JVM?一文看懂 GraalVM Native Image、Substrate VM 与 JDK

平时我们运行 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 产品

需要特别注意两点:

  1. OpenJDK 主线本身不包含 Substrate VM。 某个基于 OpenJDK 的厂商发行版,可以额外打包 Native Image 能力,但不能因此说"所有 OpenJDK 都自带 Substrate VM"。
  2. 使用 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

第二行只是示例名称,真实格式通常由 artifactIdversion 组成。

如果本机是 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。

正确做法通常是:

  1. 优先使用框架和依赖官方提供的 Native Image 支持;
  2. 只为确实使用的动态内容补元数据;
  3. 在最终 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 突然不需要运行时了",而是把大量运行时工作提前到了构建期。

可以记住下面四句话:

  1. JAR 需要 JVM 将字节码变成 CPU 能执行的机器码。
  2. Native Image 在构建期完成静态分析和 AOT,并生成平台相关可执行文件。
  3. GC、线程、异常等运行时组件会进入最终产物,所以 ./app 可以独立于外部 HotSpot/JDK 启动。
  4. 启动快、内存低是常见优势,不是无条件结论;真正选型要看动态兼容性、稳态性能和总成本。

参考资料(官方)

文中命令用于解释构建链路。正式项目请固定并验证 JDK、GraalVM、框架、插件、操作系统、libc 和 CPU 架构版本。

相关推荐
Sylvia33.2 小时前
火星数据体育API|一站式接入足球篮球电竞等18+项目实时数据
java·开发语言·python·websocket·游戏
李少兄2 小时前
解决 MySQL Lock wait timeout exceeded 报错
java
va学弟2 小时前
Java 语言特性:泛型(Generics)
java·泛型
SL_staff3 小时前
插单风险的数据闭环治理:JVS-BI 在制造业实时预警中的技术实践
java·数据分析·数据可视化
白远山3 小时前
健身场馆无人自动化解决方案:从架构到落地实践
java·开发语言·数据库·数据挖掘·需求分析
user_admin_god3 小时前
第 01 篇:OpenAI 兼容 API 初探 —— 用 curl 跑通第一次对话
java·人工智能·spring boot·语言模型·devops
曹牧3 小时前
PostMan:400 Bad Request‌
java
devpotato3 小时前
RPO与RTO:容灾的两个关键指标
java·后端
宁渡AI大模型4 小时前
AI 全栈面试新趋势:Vibe Coding、前端、Java 后端高频面试题深度解析|河南宁渡科技有限公司编程教程
java·javascript·人工智能·python·ai大模型