启动慢排查指南:Bean 初始化耗时 2 分钟?Profile 分析+懒加载优化,提速 50%!
公司一个微服务,启动一次要两分多钟。每次发布都是煎熬——CI/CD 等两分钟,滚动更新每起一个新 Pod 又是两分钟,发个版十分钟起步。他们以为是 Spring Boot 就这样,直到有一天我在控制台加了一行 -Dspring-startup-analyzer,发现有个 Bean 的 @PostConstruct 里竟然在同步加载全量字典数据,光它一个就花了 40 秒。
Spring Boot 启动慢这件事,大多数时候不是你 Bean 太多,而是有几个 Bean 初始化的时候干了不该干的活。今天聊聊怎么把这几颗老鼠屎找出来,以及怎么做懒加载优化。
先定位:到底是哪些 Bean 在拖后腿
Spring Boot 启动慢,第一件事不是优化,是找到瓶颈。没有数据支撑的优化就是瞎改。
最简单的方式:Spring Boot Actuator 的 Startup 端点
Spring Boot 2.4+ 内置了一个 ApplicationStartup 机制。在配置文件里开一下就能用:
# application.yml
spring:
application:
startup:
step-recorder:
enabled: true
然后调用 Actuator 端点查看每一步的耗时:
curl http://localhost:8080/actuator/startup
返回的数据是完整的启动步骤树,每个 Bean 初始化耗了多少毫秒,一目了然。直接找到耗时最高的前几个 Bean,逐个排查。
但这个端点默认只保留最近 1024 个步骤。如果你的项目有几千个 Bean,步骤会截断。调大缓冲区:
management:
endpoint:
startup:
max-steps: 5000
更直观的方式:spring-startup-analyzer
这是一个开源工具,不需要改代码。启动的时候加一行 JVM 参数就行:
java -javaagent:spring-startup-analyzer.jar -jar your-app.jar
启动完成后会在项目目录下生成一个 HTML 报告,用火焰图和时间线的形式展示每个 Bean 的初始化耗时。比看 JSON 数据直观得多——一眼就能看出哪个 Bean 占了一大块红色。
最土的方法:System.currentTimeMillis()
如果不想引入任何外部工具,最原始的办法也有效:在可疑的 @PostConstruct 或者 InitializingBean.afterPropertiesSet() 里掐一下时间:
@Component
class SlowBean:
@PostConstruct
init():
start = System.currentTimeMillis()
加载全量字典数据()
log("SlowBean 初始化耗时: {}ms", System.currentTimeMillis() - start)
朴素,但有效。特别是当你知道大概哪些地方可能慢的时候。
常见慢 Bean 的四种模式
定位到慢的 Bean 之后,你会发现它们几乎都落入这四种模式之一:
模式一:初始化时加载全量数据
@Component
class DictCache:
@PostConstruct
init():
// 启动时从数据库全量加载 10 万条字典 → 40 秒
this.data = dictMapper.selectAll()
这是最常见的坑。数据量不大还好,字典表膨胀到几万条之后,初始化时间线性增长。而且数据库连接也是启动时才建好的,SQL 执行可能还在冷启动阶段,更慢。
解法:懒加载。第一次用到的时候再去查,而不是启动时全量加载。
模式二:初始化时建立外部连接
@Component
class MQConsumer:
@PostConstruct
init():
// 连接 RabbitMQ → 如果 MQ 慢了,这里会卡很久
connection = factory.newConnection()
// 创建拓扑(Exchange、Queue、Binding)→ 又卡一轮
channel.exchangeDeclare(...)
外部连接本身就慢,如果对端服务响应慢或者有重试逻辑,启动时间会被拖到不可控。
解法:连接建立延后到首次使用,或者至少做成异步不阻塞 Spring 容器启动。
模式三:初始化时做复杂计算
@Component
class GeoIndex:
@PostConstruct
init():
// 从文件加载 500MB 的地理数据 → 构建空间索引 → 3 分钟
this.tree = buildRTree(loadGeoData())
计算密集型任务不适合放在启动路径上。
解法:懒加载 + 异步预热。首次请求时可能慢一点,但至少服务能先起来接流量。
模式四:依赖链过长
A 的 @PostConstruct 等 B 初始化完,B 等 C 初始化完,C 等数据库连接池初始化完。一条链上串了七八个 Bean,一个慢了全线慢。
解法:砍断不必要的依赖。很多 Bean 在构造阶段就依赖另一个 Bean,但其实它只是需要一个代理或者空壳,不是真正需要对方已经完全初始化。用 @Lazy 注解打破循环依赖的同时也能让初始化顺序变灵活。
懒加载:让 Bean 从"启动时创建"变成"用到时再创建"
Spring Boot 默认所有单例 Bean 都在启动时实例化。这就是为什么 Bean 越多、启动越慢。
@Lazy 注解可以让 Bean 延后到第一次被注入时才创建:
@Component
@Lazy
class DictCache:
// 这个 Bean 不会在启动时初始化
// 第一个请求进来、第一次用到 DictCache 的时候才创建
你可以全局开启懒加载:
spring:
main:
lazy-initialization: true
全局懒加载之后,所有 Bean 都延后创建,启动时间会大幅缩短。但代价是——第一个请求会变慢,因为它要触发所有用到的那条链上的 Bean 初始化。而且全局懒加载会导致一些启动时的校验被跳过(比如配置缺失、依赖不满足),这些问题会延迟到首次请求才暴露。
更好的做法是按需懒加载:只对那几个启动慢的 Bean 加 @Lazy,其他的保持急加载。这样既保留了启动校验的好处,又把慢的 Bean 甩出启动路径。
实战:一条完整的启动优化链路
假设通过 Startup 端点分析,发现下面三个 Bean 占启动耗时的 80%:
ApplicationStartup 报告:
dictDataCache.init() → 42s ← 全量加载字典
elasticsearchClient.init() → 18s ← 建 ES 连接、校验索引
geoIndexBuilder.init() → 25s ← 加载地理数据、建索引
─────────────────────────────────
总计 → 85s(占启动总时间 80%)
优化策略:
dictDataCache:
@Lazy + 首次访问时异步加载 → 不阻塞启动
elasticsearchClient:
@Lazy + 连接池延迟连接 → 不阻塞启动
geoIndexBuilder:
@Lazy + 异步预热线程 → 服务启动后后台慢慢构建
改动很简单,就是加 @Lazy。效果:
优化前:启动 110s
优化后:启动 35s(提速 68%)
关键改造:
// 优化前
@Component
class DictCache:
@PostConstruct
init():
this.data = loadFromDB() // 启动时卡 42 秒
// 优化后
@Component
@Lazy
class DictCache:
private volatile boolean loaded = false
get(key):
if not loaded:
synchronized(this):
if not loaded:
this.data = loadFromDB() // 首次访问时加载
loaded = true
return data.get(key)
Double-check locking 保证线程安全,只加载一次。首次请求会多等一会儿,但比起每次重启等两分钟,这个代价完全可以接受。
别忘了关掉不用的自动配置
Spring Boot 的 @EnableAutoConfiguration 会根据 classpath 里的依赖自动加载对应的配置。如果你引入了 spring-boot-starter-data-redis 但没用到 Redis,它的自动配置还是会跑一遍——连 Redis、校验连接、初始化模板。
排查方式很简单:看启动日志里有没有你不认识的 Bean 在初始化。
# 启动日志里搜 "Auto-configuration"
Auto-configuration report:
RedisAutoConfiguration matched:
- @ConditionalOnClass: RedisOperations found
排除不需要的自动配置:
@SpringBootApplication(exclude = {
RedisAutoConfiguration.class,
ElasticsearchDataAutoConfiguration.class
})
少加载一个不必要的自动配置,就少创建几十个 Bean,少一次网络连接尝试。积少成多。
总结
Spring Boot 启动慢,根因不是框架本身重,而是某些 Bean 在初始化的时候做了不该做的重活。
排查思路:先用 Startup 端点或工具定位慢 Bean,再看它们是哪种模式——加载数据、建连接、复杂计算、还是依赖链。该加 @Lazy 的加 @Lazy,该关自动配置的关掉,该异步预热的异步。
别一上来就全局懒加载。按需懒加载才是正解——慢的几个 Bean 延迟创建,剩下的照常启动,启动校验功能不受影响。
另外有个认知很重要:启动慢不是 Bug,但不知道哪里慢才是。 哪怕你暂时不改,至少要知道耗时分布。下次别人问你"为什么启动要两分钟",你能精确到哪个 Bean 花了多少秒,而不是两手一摊说 Spring Boot 就这样。
有用的话转给还在每次启动去接杯水的同事。
标题:启动慢排查指南:Bean 初始化耗时 2 分钟?Profile 分析+懒加载优化,提速 50%!
作者:jiangyi
地址:http://jiangyi.space/articles/2026/06/06/1780413369360.html
公众号:服务端技术精选
评论