启动慢,时间到底花在哪?懒加载省多少、代价是什么?AppCDS 值不值得用?这篇用 ApplicationStartup 给启动打点,再用同一个应用逐项对照懒加载、JVM 参数和 AppCDS 动态归档。
各阶段(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 msrefresh 阶段占了 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 次请求变慢,内存几乎没变。
-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=409C1-only 启动略快、内存更小;SerialGC 启动没变快;稳定负载下的代价本篇没测。
训练 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 次请求也会受益;能把启动时间降约三成。