![图片[1]-36 SubstrateVM深度解析:Java程序也能启动如闪电、内存占用如C?-速优课](http://www.suyouke.com/wp-content/uploads/2026/07/doubao_img_2304x1728_20260722_174942-1-1024x768.png)
本文导读
你有没有抱怨过 Java 程序启动慢、内存占用高?在 Serverless、微服务这些需要快速弹性伸缩的场景下,Java 的这个缺点被无限放大。有没有办法让 Java 程序既能享受 JVM 的生态,又能像 C 程序一样启动快、占内存小?
答案是有的——AOT 编译。本文将带你深入了解 GraalVM 中的 AOT 编译框架 SubstrateVM,看看它是如何让 Java 程序实现毫秒级启动、兆级内存占用的。
通过阅读本文,你将了解:
- AOT 编译与 JIT 编译的区别与各自的优劣
- Java 9 引入的 jaotc 工具是怎么回事
- SubstrateVM 的设计理念与实现原理
- SubstrateVM 为什么能做到启动快、占内存小
- Metropolis 项目与 Java-on-Java 的未来
一、AOT 编译概述
1.1 什么是 AOT 编译
AOT(Ahead-Of-Time,提前编译)是和 JIT(Just-In-Time,即时编译)相对的概念。
我们已经很熟悉 JIT 了:在程序运行过程中,把字节码编译成可以直接在硬件上运行的机器码,然后部署到代码缓存中。
而 AOT 编译则是:在程序运行之前,就把字节码转换成机器码。它的产物可以是:
- 需要链接到托管环境中的动态共享库
- 可以直接独立运行的可执行文件
狭义的 AOT 编译针对的代码和 JIT 一致,也就是那些原本可以被 JIT 编译的代码。不过我们也可以简单地把 AOT 编译器理解为类似于 GCC 那样的静态编译器。
1.2 AOT 编译的优劣
AOT 编译的优点非常直观:
- 运行时不需要耗费 CPU 资源做即时编译
- 程序启动就能达到很好的性能
但和 JIT 相比,AOT 也有明显的劣势:
- 无法获得程序运行时的信息
- 不能做基于类层次分析(CHA)的完全虚方法内联
- 不能做基于程序 profile 的投机性优化
当然这些不是硬性限制——可以通过限制运行范围,或者利用上一次运行收集的 profile 来绕开这些限制,但终究不如 JIT 那么灵活。
这些都会影响程序的峰值性能。所以 AOT 和 JIT 是一种权衡:AOT 换来了启动速度,牺牲了部分峰值性能;JIT 则需要时间预热,但峰值性能更高。
1.3 Java 9 的 jaotc 工具
Java 9 引入了实验性的 AOT 编译工具 jaotc。它借助 Graal 编译器,把输入的 Java 类文件编译成机器码,存放到生成的动态共享库中。
启动时,JVM 通过 -XX:AOTLibrary 参数加载指定的动态共享库,部署其中的机器码。这些机器码的工作机制和 JIT 生成的机器码一样:在方法调用时切入,也支持去优化回到解释执行。
但是,JVM 可能通过 Java Agent 或 C Agent 修改加载的字节码,或者 AOT 编译的是旧版本的类。因此需要额外的验证机制,确保机器码的语义和当前加载的类一致。
jaotc 使用的是类指纹(class fingerprinting)机制:
- 在动态共享库中保存被 AOT 编译的类的摘要信息
- 运行时 JVM 把摘要和已加载的类做比较
- 如果不匹配,直接舍弃这份 AOT 编译的机器码
jaotc 的一个典型应用场景是编译 java.base 模块——也就是 Java 核心类库中最基础的那些类。这些类几乎一定会被用到,但调用频率未必高到能触发 JIT 编译。提前把它们编译好,可以避免在执行 JIT 编译代码时,因为调用了这些基础类而切换到解释执行的性能惩罚。
不过今天的主角不是 jaotc,而是同样使用 Graal 编译器、但走得更远的 SubstrateVM。
二、SubstrateVM 的设计与实现
2.1 SubstrateVM 是什么
SubstrateVM 的设计初衷是提供一个:
- 高启动性能
- 低内存开销
- 能无缝衔接 C 代码
的 Java 运行时。
它和 jaotc 主要有两个区别:
区别一:独立的运行时
jaotc 生成的代码还是跑在 HotSpot 虚拟机里的。而 SubstrateVM 脱离了 HotSpot,拥有自己独立的运行时,包括:
- 异常处理
- 同步机制
- 线程管理
- 内存管理(垃圾回收)
- JNI 等组件
区别二:封闭性假设
SubstrateVM 要求目标程序是封闭的——也就是说不能动态加载其他类库。基于这个假设,SubstrateVM 可以:
- 探索整个编译空间
- 通过静态分析推算出所有虚方法调用的目标方法
- 把所有可能执行到的方法都纳入编译范围
这样做的好处是:不需要实现解释执行器,所有代码都是编译好的机器码。
2.2 SubstrateVM 的组成
从执行时间来划分,SubstrateVM 分为两部分:
| 部分 | 作用 |
|---|---|
| native image generator | 包含真正的 AOT 编译逻辑。它本身是一个 Java 程序,使用 Graal 编译器把 Java 类文件编译成可执行文件或动态链接库 |
| SubstrateVM 运行时 | 精简版的 Java 运行时,AOT 编译后的目标程序跑在这个运行时之上 |
2.3 编译过程:指针分析与堆快照
在编译之前,native image generator 会做一件非常重要的事情:指针分析(points-to analysis)。
它从用户提供的程序入口出发,探索所有可达的代码。探索的同时,它还会执行初始化代码。最终生成可执行文件时,把已经初始化好的堆保存成一个堆快照(heap snapshot)。
这意味着什么?意味着 SubstrateVM 启动时不需要再走一遍 JVM 的初始化流程,可以直接从目标程序开始运行。这也是 SubstrateVM 启动快的关键原因之一。
2.4 适用范围
SubstrateVM 主要用于 JVM 语言的 AOT 编译,比如 Java、Scala、Kotlin。
Truffle 语言实现本质上也是 Java 程序,而且它用到的所有类都是编译时已知的,所以也适合跑在 SubstrateVM 上。不过注意:SubstrateVM 不会 AOT 编译用 Truffle 语言写的用户程序,只会 AOT 编译 Truffle 语言实现本身。
三、启动时间与内存开销对比
3.1 Hello World 对比
SubstrateVM 的启动时间和内存开销到底有多低?我们来看一组对比数据。
Java vs C 的 Hello World:
| 实现 | 执行时间 | 内存开销 |
|---|---|---|
| C 程序 | < 10ms | < 500KB |
| Java on HotSpot | ~40ms | ~24MB |
| Java on SubstrateVM | < 10ms | ~850KB |
可以看到:
- HotSpot 上的 Java 程序需要 40ms 启动,占 24MB 内存
- SubstrateVM 上的 Java 程序启动时间和 C 程序持平,内存开销只有 850KB 左右
这都得益于 SubstrateVM 保存的堆快照,以及不需要额外初始化、直接执行目标代码的特性。
3.2 JavaScript 引擎对比
再来看一个更”重型”的对比——JavaScript 引擎。测试对象是 Google V8 和基于 Truffle 的 Graal.js:
| 实现 | 执行时间 | 内存开销 |
|---|---|---|
| V8 | < 10ms | ~18MB |
| Graal.js on HotSpot | ~650ms | ~120MB |
| Graal.js on SubstrateVM | < 10ms | ~4.2MB |
结果非常惊人:
- HotSpot 上的 Graal.js 需要 650ms 才能启动,占 120MB 内存(因为要启动 JVM + 预热 Graal 编译器)
- SubstrateVM 上的 Graal.js 启动时间和 V8 持平,而内存开销只有 4.2MB——远小于 V8 的 18MB
由于 SubstrateVM 轻量的特性,它非常适合嵌入到其他系统中。Oracle Labs 的另一个团队就把 Truffle 语言实现嵌入到了 Oracle 数据库中,这样就可以在数据库里用任意语言写存储过程了——这就是 Oracle Database Multilingual Engine(MLE)。
四、Metropolis 项目:Java-on-Java 的未来
4.1 为什么需要 Java-on-Java
OpenJDK 有一个叫 Metropolis 的项目,目标是实现”Java-on-Java”——用 Java 来写 Java 虚拟机。
你可能会问,现在 HotSpot 不是用 C++ 写的吗?为什么要换成 Java?
确实,目前 HotSpot 绝大部分代码都是 C++ 写的。这造就了一个很有意思的现象:想为 Java 语言本身做贡献,你得先精通 C++。而且随着 HotSpot 项目越来越庞大,维护难度也在不断上升。
Oracle 架构师 John Rose 提出了用 Java 开发 JVM 的四大好处:
- 完全控制优化技术:能够完全控制编译 JVM 时使用的优化技术
- 解耦 C++ 语言更新:不需要跟着 C++ 语言的更新走
- 降低开发维护门槛:减轻开发人员和维护人员的负担(毕竟 Java 开发者比 C++ 多得多)
- 更敏捷的功能迭代:能够以更敏捷的方式实现 Java 的新功能
4.2 之前的尝试
Metropolis 并不是第一个提出 Java-on-Java 概念的项目。实际上,Jikes RVM 和 Maxine VM 都已经用 Java 完整实现了一套 JVM(Maxine VM 的即时编译器 C1X 就是 Graal 编译器的前身)。
但 Java-on-Java 技术有个老问题:它会干扰应用程序的垃圾回收和即时编译优化,从而严重影响启动性能。
举两个例子:
- 堆空间竞争:使用 Graal 编译器的 HotSpot,在 JIT 编译过程中会生成大量 Java 对象。这些对象同样占据应用程序的堆空间,导致 GC 更频繁。
- 编译线程竞争:Graal 编译器本身也会触发 JIT 编译,和应用程序的编译抢 CPU 资源。这导致应用程序从解释执行切换到编译执行的时间大大拉长,降低了启动性能。
4.3 Metropolis 的思路
Metropolis 项目的第一个子项目,就是探索部署 AOT 编译后的 Graal 编译器的可能性。具体做法是借助 SubstrateVM 技术,把整个 Graal 编译器 AOT 编译成机器码。
这样做有两个好处:
- 启动更快:运行时 Graal 编译器不需要再被 JIT 编译了,也就不会再抢占本应用于编译应用程序的 CPU 资源。使用 Graal 的 HotSpot 虚拟机的启动性能会得到大幅提升。
- 堆空间隔离:SubstrateVM 编译出来的 Graal 编译器使用独立的堆空间。编译器在 JIT 过程中生成的对象,不会再干扰应用程序的堆空间。
目前 Metropolis 项目还处于前期验证阶段,后续发展值得关注。
总结与思考
本文要点回顾
- AOT vs JIT:
- AOT 在运行前编译,启动快,但峰值性能可能略低
- JIT 在运行时编译,需要预热,但峰值性能更高
- 两者是不同场景下的权衡选择
- Java 9 的 jaotc:
- 基于 Graal 编译器,把类编译成动态共享库
- 运行时加载使用,支持去优化
- 通过类指纹机制验证兼容性
- 适合编译 Java 核心类库等基础代码
- SubstrateVM 的设计:
- 脱离 HotSpot,拥有独立的运行时(GC、线程、异常等)
- 封闭性假设:不能动态加载类
- 静态分析所有可达代码,全部 AOT 编译
- 不需要解释执行器
- 启动快、占内存小的秘诀:
- 编译时执行初始化,保存堆快照
- 启动时直接从程序入口开始执行,跳过 JVM 初始化
- Hello World 级别的程序启动时间和 C 持平,内存仅 ~850KB
- Metropolis 与 Java-on-Java:
- 目标是用 Java 重写 JVM,降低开发维护门槛
- 用 SubstrateVM AOT 编译 Graal,解决启动性能问题
- 编译器使用独立堆空间,不干扰应用堆
思考与实践
- AOT 和 JIT 各有优劣,你觉得在你的业务场景中,哪个更重要?为什么?
- SubstrateVM 的封闭性假设(不能动态加载类)限制了它的应用范围,你觉得这个限制可以怎么突破?
- 如果未来 JVM 真的完全用 Java 重写了,你认为会带来哪些影响?
SubstrateVM 为 Java 生态打开了一扇新的大门——在 Serverless、微服务、嵌入式等场景下,Java 不再因为启动慢、占内存而被诟病。它让 Java 既可以享受成熟的生态,又能拥有原生程序的启动速度和内存效率。








请登录后查看评论内容