我们都知道,在java的世界,有一个神奇的存在,那就是虚拟机(jvm)。
因为虚拟机的存在,java实现了最早的“一处编译,处处执行”的设计思想,成功在早期的各大语言的竞争中脱颖而出,并在各种后起之秀的冲击下,长期保持最受欢迎的编程语言的头部区域。
实在是太方便了,只要有虚拟机的存在,我们在编程的时候就不用考虑各种运行系统的复杂性与差异,这些由各种系统的虚拟机开发专家们替我们解决了,而我们仅需要关注编码就行了。
但进入了21时间,云原生的兴起,java的最大的优势虚拟机,也渐渐阻碍了其在云上开疆扩土的阻力。
“内存占用高”、“STW” 、 “启动速度慢”等一些虚拟机的固有特性,导致java在go等这些为云而设计的语言面前尽显颓势。
所以,java能不能跳过虚拟机,直接把程序编译打包成可以直接在目标系统上运行的呢?
说实话,刚开始学习java的时候,我就有这个疑问,为了把java的可执行程序(jar)打包成可以在windows上运行的exe程序,当时还搞过很多偏门的的办法。
现在,随着GraalVM的成熟,springboot的也提出了直接将java程序直接打包成系统可执行程序(本地应用)的技术方案。

今天我们就来验证一下!
实现过程
hello world
先写一套hello world
的springboot程序,使用maven或gradle打包。
这里和我们正常的项目开发流程完成一致,没有任何不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @RestController @SpringBootApplication public class GraalvmNativeAppApplication {
@RequestMapping("/{name}") String home(@PathVariable String name) { String x = "Hello " + name; System.out.println(x); return x; } public static void main(String[] args) { SpringApplication.run(GraalvmNativeAppApplication.class, args); }
}
|
这是这个项目的业务代码,没有其他的了。
配置GraalVM
在pom.xml
里配置GraalVM 本地应用打包插件,这是我们的springboot程序能够打包成本地应用的核心入口,一定要有。
1 2 3 4 5 6 7 8 9 10
| <build> <plugins> <plugin> <groupId>org.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId> </plugin> </plugins> </build>
|
其他的,项目里就不需要了。
也可以通过start.spring.io来构建项目,spring会把一切配置好,记得看构架好后的HELP.MD
文件呦。

环境依赖
接下来就是要配置一下系统环境了。
要将项目打包成系统原生运行应用,需要在我们的系统里安装一套本地镜像支持库(Liberica Native Image Kit page),这个nik库实现了将java字节码编译成操作系统二进制执行码这一过程。
安装nik包可以手动安装,也可以使用sdk版本控制工具来安装, 这个比较简单,我使用的是这种方式(我使用的是mac电脑)。
使用sdk安装java nik 的命令如下:
1
| sdk install 23.0.7.r17-nik
|
安装完成后,系统就自动将当前的jdk切换成刚下载的这个jdk了。
1 2 3 4 5
| % java --version
openjdk 17.0.14 2025-01-21 LTS OpenJDK Runtime Environment Liberica-NIK-23.0.7-1 (build 17.0.14+10-LTS) OpenJDK 64-Bit Server VM Liberica-NIK-23.0.7-1 (build 17.0.14+10-LTS, mixed mode, sharing)
|
出现这个信息,我们就可以打包了。
打包
命令行输入
1
| mvn -Pnative native:compile
|
这个命令会触发native-maven-plugin
maven插件,执行本地打包app。
这个过程会很慢,而且会很耗cpu,我的电脑cpu基本都跑满了,现在终于理解,那些c程序员们在编译项目时会去抽烟了,确实这段时间啥也干不了。
执行过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| ... [1/8] Initializing... (3.9s @ 0.14GB) Java version: 17.0.14+10-LTS, vendor version: Liberica-NIK-23.0.7-1 Graal compiler: optimization level: 2, target machine: armv8-a C compiler: cc (apple, arm64, 15.0.0) Garbage collector: Serial GC (max heap size: 80% of RAM) 1 user-specific feature(s) - org.springframework.aot.nativex.feature.PreComputeFieldFeature SLF4J(W): No SLF4J providers were found. SLF4J(W): Defaulting to no-operation (NOP) logger implementation SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details. [2/8] Performing analysis... [******] (35.5s @ 1.50GB) 16,529 (90.73%) of 18,218 types reachable 27,176 (67.68%) of 40,151 fields reachable 79,715 (63.21%) of 126,116 methods reachable 5,144 types, 357 fields, and 5,957 methods registered for reflection 64 types, 72 fields, and 55 methods registered for JNI access 5 native libraries: -framework CoreServices, -framework Foundation, dl, pthread, z [3/8] Building universe... (3.9s @ 1.59GB) [4/8] Parsing methods... [**] (2.7s @ 1.79GB) [5/8] Inlining methods... [****] (1.6s @ 1.95GB) [6/8] Compiling methods... [*****] (23.4s @ 1.61GB) [7/8] Layouting methods... [**] (4.5s @ 1.70GB) [8/8] Creating image... [***] (7.9s @ 1.29GB) 36.95MB (50.32%) for code area: 51,915 compilation units 35.50MB (48.34%) for image heap: 386,138 objects and 253 resources 1006.98kB ( 1.34%) for other data 73.44MB in total ------------------------------------------------------------------------------------------------------------------------ Top 10 origins of code area: Top 10 object types in image heap: 12.68MB java.base 8.53MB byte[] for code metadata 4.24MB tomcat-embed-core-10.1.36.jar 3.95MB java.lang.Class 3.38MB java.xml 3.68MB java.lang.String 2.00MB jackson-databind-2.18.2.jar 3.30MB byte[] for java.lang.String 1.58MB spring-core-6.2.3.jar 3.23MB byte[] for general heap data 1.54MB spring-boot-3.4.3.jar 2.11MB byte[] for embedded resources 1.29MB svm.jar (Native Image) 1.39MB com.oracle.svm.core.hub.DynamicHubCompanion 983.58kB spring-web-6.2.3.jar 1.01MB byte[] for reflection metadata 910.85kB spring-beans-6.2.3.jar 741.69kB java.lang.String[] 876.34kB spring-webmvc-6.2.3.jar 597.07kB c.o.svm.core.hub.DynamicHub$ReflectionMetadata 7.23MB for 69 more packages 6.15MB for 3522 more object types ------------------------------------------------------------------------------------------------------------------------ Recommendations: HEAP: Set max heap for improved and more predictable memory usage. CPU: Enable more CPU features with '-march=native' for improved performance. ------------------------------------------------------------------------------------------------------------------------ 10.3s (12.1% of total time) in 171 GCs | Peak RSS: 2.64GB | CPU load: 5.10 ------------------------------------------------------------------------------------------------------------------------ Produced artifacts: ../springboot3-demo/graalvm-native-app/target/graalvm-native-app (executable) ======================================================================================================================== Finished generating 'graalvm-native-app' in 1m 24s. [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 01:38 min [INFO] Finished at: 2025-03-03T16:41:31+08:00 [INFO] ------------------------------------------------------------------------
|
运行app
验证:
1 2 3
| $ curl localhost:8080/hancher
Hello hancher
|
优缺点
先说优点
- 去掉了jvm这一层,部署更加简单方便了
- 通过app这种形式,原生级别支持了windows、mac、linux等主流操作系统
- 启动更快,因为代码都编译成系统直接运行的二进制文件,不需要jvm的解析
- 上面的helloWorld,使用jar启动,耗时0.9s,使用app启动,耗时0.18秒,将近5倍的差距。
- 运行时占用内存更低,据统计,能减少50%左右
再说缺点
- 打包很慢,会占用很高的内存和cpu
- 对于反射等动态特性支持的不好,比如
Class.forName()
, 所以仅适用于一些简易项目,重量级项目现阶段还不太支持
- 一个系统仅能打包本系统的app,不像go那样,一个系统能打包所有平台的app。
- 包体积比原生的jar大了不少
- 上面的helloWorld,jar大小空间20M,app大小73M, 大了3倍
- 但如果算上jdk, 我统计了我安装后的jdk17大小300M,再加上jar, 使用jar的总体空间比app大了近5倍
总得来说,java在编译成本地app这一块,还有很大的空间,但已经完成了从0到1的进步,更适合云原生了,剩下的就是不断的优化了。
适用的场景:
- 新项目,轻量级项目。老项目不太建议使用这个特性,因为动态特性的限制,可能会有一些功能验证不到
- 类似云原生的Serverless架构,逻辑简单,更强调启动速度与内存占比的服务
- 容器化部署的微服务等(native app 有容器版,下篇文章介绍)
参考
使用GraalVM打包系统可执行程序
github demo