Spring Boot 4.2.0-RC1 把可执行 fat jar 里的类和资源查找改成直接走 nested jar,官方还顺手优化了 deflated 嵌套条目的 readAllBytes()。我拿同一套小 Web 工程分别打 4.1.2 和 4.2.0-RC1 的可执行 jar,冷启动各计 5 轮。Ready 中位数从 1.853 秒到 1.811 秒,只快了约 0.04 秒,落在轮次波动里,几乎看不出。
改动面向的是 java -jar 这条 JarLauncher 路径,不是把 fat jar 解压后再跑。解压后再训 AOT 缓存是另一条线,之前那篇 fat jar 做 AOT 几乎没效果、解压后中位数从 1.23 秒降到 0.54 秒 已经写过。
RC1 改了什么
4.2.0-RC1 发布说明里,Startup Performance Improvements 写得很直白:可执行 jar 里的类和资源现在直接在 nested jar 里查,不再每次查找都给每个 nested jar 开一层 URL 连接。读 deflated 嵌套条目、解析缓存资源也做了优化。
对应两个已合入的 PR:
- #52246:
JarUrlClassLoader通过NestedJarFile直接查类和资源。以前URLClassPath会对每个 nested jar 建 URL、开URLConnection,大量查找其实是 miss。现在只在条目真的存在时才建 URL。 - #52285:把
readAllBytes()优化落到 deflated 嵌套条目常用的 inflater 流上。PR 描述里 PetClinic 启动到 Ready 的分配从约 651 MB 降到 442 MB,启动时间本身几乎没再动。
#52246 相关讨论里,作者用 PetClinic(约 92 个 nested jar)和带 Web/JPA/Security/Actuator 的样例(约 101 个)测过:在 --cpus=2 下启动大约快 8% 到 13%,启动期分配少约一半。我们这次故意用更小的业务包,看看日常小服务能不能摸到这个量级。
同一业务,两套可执行 jar
工程依赖一样:spring-boot-starter-web、actuator、validation,外加 commons-lang3 和 Guava。业务侧有几个自有类、一个 64 KB 的 classpath 资源、两份静态文件。源码两份目录,只换 parent 版本。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.2</version>
</parent>
另一份把 version 换成 4.2.0-RC1。两边都能从 Maven Central 解析。JDK 固定本机这套:
$ java -version
openjdk version "21.0.12.1" 2026-08-18
OpenJDK Runtime Environment (build 21.0.12.1+1-1-deb13u1-Debian)
OpenJDK 64-Bit Server VM (build 21.0.12.1+1-1-deb13u1-Debian, mixed mode, sharing)
Maven 3.9.9,mvn -DskipTests package 打出的都是 JarLauncher 可执行包,约 27 MB,BOOT-INF/lib 下各 53 个依赖 jar:
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.example.demo.DemoApplication
Spring-Boot-Version: 4.1.2
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.example.demo.DemoApplication
Spring-Boot-Version: 4.2.0-RC1
外层 fat jar 里这些 nested jar 条目本身是 Stored(不压缩),方便按偏移直接 seek。真正走 deflated 的是 nested jar 内部的 .class,所以 #52285 仍然有意义。
冷启动怎么计
同一台机器、同一 JDK,先各丢弃 1 轮热身,再正式计 5 轮。每轮:
java -jar启动可执行包,端口 18080- 等日志出现
Started DemoApplication in … seconds,记下 Ready 时间,同时记下process running for - 用
date +%s%3N包住整段启动,得到 wall-clock 毫秒 curl/healthz回 200,再打一下/,然后杀进程
主结论只用 java -jar,不用 mvn spring-boot:run。
启动成功后 / 长这样:
{"app":"nested-jar-startup-demo","report":"nested-jar-lookup-2026-10-31T00:12:40.938302675Z","resourceBytes":65536}
5 轮数字
4.1.2 正式 5 轮的 Started 行,原样如下:
Started DemoApplication in 1.681 seconds (process running for 2.021)
Started DemoApplication in 1.853 seconds (process running for 2.196)
Started DemoApplication in 1.983 seconds (process running for 2.363)
Started DemoApplication in 1.813 seconds (process running for 2.162)
Started DemoApplication in 1.955 seconds (process running for 2.294)
4.2.0-RC1 正式 5 轮:
Started DemoApplication in 1.518 seconds (process running for 1.833)
Started DemoApplication in 1.811 seconds (process running for 2.202)
Started DemoApplication in 2.118 seconds (process running for 2.492)
Started DemoApplication in 1.712 seconds (process running for 2.015)
Started DemoApplication in 2.323 seconds (process running for 2.694)
汇总:
版本 Ready 各轮(s) min median avg
4.1.2 1.681 1.853 1.983 1.813 1.955 1.681 1.853 1.857
4.2.0-RC1 1.518 1.811 2.118 1.712 2.323 1.518 1.811 1.896
版本 wall-clock 各轮(ms) min median avg
4.1.2 2171 2430 2513 2328 2503 2171 2430 2389
4.2.0-RC1 1961 2392 2648 2191 2869 1961 2392 2412
版本 process running for 各轮(s) min median avg
4.1.2 2.021 2.196 2.363 2.162 2.294 2.021 2.196 2.207
4.2.0-RC1 1.833 2.202 2.492 2.015 2.694 1.833 2.202 2.247
Ready 中位数 RC1 快 0.042 秒,大约 2.3%。平均值反而慢了 0.039 秒,因为 RC1 有一轮到了 2.323 秒。wall-clock 和 process 耗时也是同一量级:中位数几乎贴着,平均值差几个百分点,单轮高低差却有 0.5 秒上下。共享机器上还有别的负载,这种抖动正常。
我后来又交错着各跑过几轮,RC1 有时能摸到 1.5 秒出头,4.1.2 也有被挤到 3 秒的脏点。结论不变:这个 53 依赖的小包上,查找优化带来的启动时间差,稳定不进「一眼能看见」的区间。
和官方样例差在哪
#52246 那边是 90 多个 nested jar,类查找上万次,miss 连接占比极高。我们这里只有 53 个依赖 jar,应用类和资源都很少,查找路径本来就短,省下来的 URLConnection 自然有限。#52285 更偏向分配,不看分配计数的话,Ready 秒数本来就不会动多少。
所以这篇不是打脸官方数字,是把场景收窄到日常小 Web fat jar 后,启动墙钟时间别指望靠 RC1 这一项省出大头。依赖更多、静态资源查找更频繁的服务,更接近 PR 里的收益画像。
要不要升 RC
就「嵌套 jar 启动查找」这一项,当前这个小项目不值得专门升到 4.2.0-RC1。差异小到被噪声盖住,RC 本身还有别的回归风险(RC1 发布说明里也列了 #52246 之后资源名带空格等后续修复)。想摸启动,解压后再配 AOT 缓存那条路幅度更大;想摸分配,得在更大应用上单独盯 allocation,而不是只看 Ready 行。
等 4.2.0 GA,或者你手头本来就是上百 nested jar 的 fat jar,再把这项优化放进升级理由里更合适。
一起交流
分享你的思考,让讨论更进一步。