bigywater.tech
Spring · 74

启动时间分析:时间花在哪里,Lazy 与 AppCDS

启动慢,时间到底花在哪?懒加载省多少、代价是什么?AppCDS 值不值得用?这篇用 ApplicationStartup 给启动打点,再用同一个应用逐项对照懒加载、JVM 参数和 AppCDS 动态归档。

889 ms
一次 1290 ms 的启动里 refresh 阶段占的时间,最慢的 Bean 都是框架自己的
166 ms ↔ 81 ms
懒加载让启动快了 166 ms,第 1 次请求慢了约 81 ms(123.0 对 41.7 算出来的差)
1083 → 759 ms
AppCDS 动态归档把启动时间从 1083 ms 降到 759 ms

时间花在哪里

实测 · 本机执行输出
  各阶段(ms):JVM启动→main 17;main→Starting 193;Starting→EnvPrepared 103;EnvPrepared→Prepared 62;Prepared→Refreshed(refresh) 889;Refreshed→Ready 41;main→Ready 1290
  singletons=425,definitions=409
  ApplicationStartup 缓冲到的步骤(名称、次数、各次耗时合计;嵌套的步骤会重复计入父步骤):
    spring.beans.instantiate:次数 409,合计 629.9 ms
    spring.boot.application.context-loaded:次数 1,合计 5.3 ms
    spring.boot.application.context-prepared:次数 1,合计 0.1 ms
    spring.boot.application.environment-prepared:次数 1,合计 76.0 ms
    spring.boot.application.started:次数 1,合计 39.1 ms
    spring.boot.application.starting:次数 1,合计 15.3 ms
    spring.boot.webserver.create:次数 1,合计 253.9 ms
    spring.context.bean-factory.post-process:次数 9,合计 3.7 ms
    spring.context.beandef-registry.post-process:次数 1,合计 334.5 ms
    spring.context.beans.post-process:次数 1,合计 386.1 ms
    spring.context.config-classes.parse:次数 1,合计 329.9 ms
    spring.context.refresh:次数 1,合计 891.2 ms
  最慢的 8 个 spring.beans.instantiate(含它依赖的 Bean 的创建时间):
    tomcatServletWebServerFactory  57.3 ms
    requestMappingHandlerMapping  39.3 ms
    webMvcObservationFilter  28.8 ms
    observationRegistry  25.3 ms
    requestMappingHandlerAdapter  22.2 ms
    mvcContentNegotiationManager  19.3 ms
    pathMappedEndpoints  19.0 ms
    defaultMeterObservationHandler  18.3 ms

refresh 阶段占了 main 到 Ready 的大头;外部计时与应用内计时只差十几毫秒,JVM 启动不是瓶颈;最慢的 Bean 全是框架的。

懒加载

实测 · 本机执行输出
  默认(非懒加载)
    进程启动到端口可连接(外部计时):中位 1139 ms(范围 1131~1198,7 次);应用内 main→Ready 中位 1121 ms
    第 1 次请求 /ping 中位 41.7 ms,第 2 次中位 1.6 ms;进程常驻内存中位 197 MB;singletons=425,definitions=409
  spring.main.lazy-initialization=true
    进程启动到端口可连接(外部计时):中位 973 ms(范围 963~984,7 次);应用内 main→Ready 中位 1057 ms
    第 1 次请求 /ping 中位 123.0 ms,第 2 次中位 1.6 ms;进程常驻内存中位 199 MB;singletons=239,definitions=409
  外部计时中位数:973 ms 对 1139 ms,相差 166 ms

启动快 166 ms,单例从 425 个降到 239 个;代价是第 1 次请求变慢,内存几乎没变。

JVM 参数

实测 · 本机执行输出
  -XX:TieredStopAtLevel=1(只用 C1 编译器)
    进程启动到端口可连接(外部计时):中位 1024 ms(范围 969~1083,5 次);应用内 main→Ready 中位 981 ms
    第 1 次请求 /ping 中位 45.5 ms,第 2 次中位 1.6 ms;进程常驻内存中位 156 MB;singletons=425,definitions=409
  -XX:+UseSerialGC
    进程启动到端口可连接(外部计时):中位 1179 ms(范围 1134~1205,5 次);应用内 main→Ready 中位 1141 ms
    第 1 次请求 /ping 中位 41.2 ms,第 2 次中位 1.6 ms;进程常驻内存中位 227 MB;singletons=425,definitions=409
  -XX:TieredStopAtLevel=1 -XX:+UseSerialGC -Xshare:auto
    进程启动到端口可连接(外部计时):中位 1033 ms(范围 1018~1080,5 次);应用内 main→Ready 中位 1016 ms
    第 1 次请求 /ping 中位 70.5 ms,第 2 次中位 1.6 ms;进程常驻内存中位 200 MB;singletons=425,definitions=409

C1-only 启动略快、内存更小;SerialGC 启动没变快;稳定负载下的代价本篇没测。

AppCDS

实测 · 本机执行输出
  训练 1:启动、请求 2 次 /ping、SIGTERM 结束 → 归档文件 43.1 MB
  训练 2:-Dspring.context.exit=onRefresh(refresh 之后立即退出,退出码 0) → 归档文件 39.3 MB
  不用归档(同一批代码,对照)
    进程启动到端口可连接(外部计时):中位 1083 ms(范围 1032~1100,7 次);应用内 main→Ready 中位 1050 ms
    第 1 次请求 /ping 中位 40.2 ms,第 2 次中位 1.6 ms;进程常驻内存中位 198 MB;singletons=425,definitions=409
  用训练 1 的归档 -XX:SharedArchiveFile
    进程启动到端口可连接(外部计时):中位 759 ms(范围 714~821,7 次);应用内 main→Ready 中位 700 ms
    第 1 次请求 /ping 中位 16.7 ms,第 2 次中位 1.7 ms;进程常驻内存中位 181 MB;singletons=425,definitions=409
  用训练 2 的归档 -XX:SharedArchiveFile
    进程启动到端口可连接(外部计时):中位 709 ms(范围 704~809,7 次);应用内 main→Ready 中位 712 ms
    第 1 次请求 /ping 中位 38.1 ms,第 2 次中位 1.8 ms;进程常驻内存中位 186 MB;singletons=425,definitions=409
  训练 1 的归档 + -XX:TieredStopAtLevel=1
    进程启动到端口可连接(外部计时):中位 655 ms(范围 645~753,5 次);应用内 main→Ready 中位 634 ms
    第 1 次请求 /ping 中位 15.2 ms,第 2 次中位 1.6 ms;进程常驻内存中位 129 MB;singletons=425,definitions=409

动态归档需要应用类打成 jar;训练时请求一次,第 1 次请求也会受益;能把启动时间降约三成。

分步演示

启动时间
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。被测应用:spring-boot-starter-webmvc + actuator + validation,150 个 @Service(每 10 个一条依赖链)和一个 /ping 接口;外部计时是 Python 从 Popen 到端口可连接的时间,每组各自重复 5 或 7 次取中位数;ApplicationStartup 打点只在第一节单次运行里打开(会带来一点额外开销);机器上没有别的负载,但不同时段的对照组会漂移几十毫秒,所以每节只在组内比较
    • 没有验证:Spring Boot 可执行 fat jar 的 CDS(需要先解包,官方有 -Djarmode=tools 的做法,本篇用的是 classpath 加 jar);JDK 24 以后的 AOT 缓存;原生镜像(见第 84 篇);连接数据库、JPA / Hibernate、Flyway 迁移等真实业务启动往往占大头的部分(本篇的应用没有数据源);懒加载把错误推迟到运行期的风险;不同 CPU 核数、容器的 CPU 限额对启动时间的影响;C1-only 在稳定负载下的吞吐代价
    京ICP备2026067502号-1京公网安备11010502063487号