启动慢排查指南: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://www.jiangyi.space/articles/2026/06/06/1780413369360.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消