Spring Boot 启动 60s 优化到 8s 实录:测量 → CDS → AOT 全路径

Spring Boot 单体老应用启动 60s,K8s 滚动更新 20 分钟。两周优化全路径:spring-startup-analyzer 测量 + 关 50 个无用 AutoConfig + 剪 jar 依赖 + Bean 懒加载 + 并行初始化 + AppCDS + 尝试 Spring AOT native。最终 8 秒,扩容 1h→10min。

2024 年我们一个 Spring Boot 单体老应用,启动要 60 秒,在 K8s 滚动更新极慢,故障恢复也慢。投了两周优化,从 60 秒压到 8 秒,具体做了:剪掉冗余依赖、关闭无用 AutoConfiguration、懒加载、CDS(Class Data Sharing)、Spring Native 编译尝试。本文复盘 Spring Boot 启动优化的完整路径,覆盖测量、剪枝、定制、AOT 编译。

事故现场

服务:商品中心(Spring Boot 2.7 + JDK 17)
依赖:120+ jar(70 个 Spring 系列)
代码量:50w 行,800 个 Controller / Service

启动日志:
$ time java -jar app.jar
... (60 秒) ...
Started Application in 58.234 seconds (JVM running for 60.871)
real    0m61.045s

时间分布:
- JVM 启动 + classpath 扫描:3s
- Spring Context 初始化:42s
- AutoConfiguration 加载:8s
- Bean 创建(包括 lazy):5s
- 网络绑定 + 健康检查:2s

业务痛点:
- K8s 滚动更新一轮(20 Pod)要 20 分钟
- 故障恢复:Pod 重启 1 分钟,业务报错
- 本地开发:每次改代码重启 1 分钟,效率低
- 大促扩容:HPA 扩 50 Pod 要 1 小时

第 1 步:测量(找瓶颈)

# 用 spring-startup-analyzer 工具
$ java -javaagent:spring-startup-analyzer.jar \
       -jar app.jar

# 输出报告(每个 Bean、AutoConfig 耗时排序)
# 示例:
[startup] BeanCreationTimeAggregator:
  dataSource:                  5234ms     # 数据库连接池初始化太慢
  redisTemplate:               2345ms
  kafkaTemplate:               2100ms
  esRestClient:                1234ms
  customSearchService:         3456ms     # 业务 Bean
  mongoTemplate:               1890ms

[startup] AutoConfigurationReport:
  RedisAutoConfiguration:       2345ms
  KafkaAutoConfiguration:       2100ms
  ElasticsearchRestClientAutoConfiguration: 1234ms
  ... 50+ AutoConfig

# 或用 Spring Boot Actuator 内置
curl http://localhost:8080/actuator/startup

# 找出 Top 10 慢 Bean,逐个优化

第 2 步:剪枝(关闭无用 AutoConfig)

// 检查实际用到哪些 AutoConfig
@SpringBootApplication(exclude = {
    // 用不上的全关掉
    KafkaAutoConfiguration.class,            // 我们用 RocketMQ
    ElasticsearchRestClientAutoConfiguration.class,  // 用 OpenSearch
    MongoAutoConfiguration.class,            // 没用 Mongo
    RedisRepositoriesAutoConfiguration.class,
    JmxAutoConfiguration.class,              // 不用 JMX
    HypermediaAutoConfiguration.class,       // 不用 HATEOAS
    HealthIndicatorAutoConfiguration.class,  // 自定义 indicator
    SecurityAutoConfiguration.class,         // 自己配
    SecurityFilterAutoConfiguration.class,
    OAuth2ClientAutoConfiguration.class,
})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

// 或更激进:application.yml
spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration
      - org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration

// 看到底有多少 AutoConfig
$ grep -c "Matched\|Did not match" target/spring-conditions.log

# 删了 30 个无用 AutoConfig,启动省 8 秒

第 3 步:剪掉冗余依赖

<!-- 找传递依赖 -->
$ mvn dependency:tree -Dverbose | grep "^\[INFO\] +"

<!-- 实际只用到的功能,移除大依赖 -->

<!-- 不好:整个 spring-data-jpa 引入,但只用一个 Repo -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- 好:用 spring-boot-starter-jdbc + 手写 SQL -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!-- 排除不用的 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>  <!-- 比 Tomcat 启动快 -->
</dependency>

<!-- 效果:jar 大小 80MB → 50MB,classpath 扫描快 2 秒 -->

第 4 步:Bean 懒加载

# application.yml
spring:
  main:
    lazy-initialization: true   # 所有 Bean 懒加载

# 启动时只创建必需的 Bean,其他用到才初始化
# 副作用:第一次访问慢(冷启动),要权衡

# 选择性懒加载(推荐):某些重的 Bean 标 @Lazy
@Service
@Lazy
public class ReportService {
    // 这个服务只在管理后台用,启动不需要
    @PostConstruct
    public void init() {
        // 预加载 50MB 报表模板
    }
}

# 排除主链路 Bean
@Configuration
public class EagerConfig {
    @Bean
    public LazyInitializationExcludeFilter eagerCore() {
        return LazyInitializationExcludeFilter.forBeanTypes(
            DataSource.class,
            RedisConnectionFactory.class,
            ServletWebServerFactory.class
        );
    }
}

第 5 步:并行初始化

// 多个独立的初始化任务并行
@Configuration
public class ParallelInitConfig {

    @Bean
    public ApplicationRunner parallelInit(
            CacheWarmer cacheWarmer,
            ConfigLoader configLoader,
            DictionaryService dictService) {

        return args -> {
            ExecutorService executor = Executors.newFixedThreadPool(4);
            try {
                CompletableFuture<Void> f1 = CompletableFuture.runAsync(
                    cacheWarmer::warmup, executor);
                CompletableFuture<Void> f2 = CompletableFuture.runAsync(
                    configLoader::loadAll, executor);
                CompletableFuture<Void> f3 = CompletableFuture.runAsync(
                    dictService::reload, executor);
                CompletableFuture<Void> f4 = CompletableFuture.runAsync(
                    this::preheatJit, executor);

                CompletableFuture.allOf(f1, f2, f3, f4).get(30, TimeUnit.SECONDS);
            } finally {
                executor.shutdown();
            }
        };
    }

    private void preheatJit() {
        // JIT 预热:启动后跑一些代码让 C2 编译
    }
}

// 数据库连接池懒建立
spring:
  datasource:
    hikari:
      initialization-fail-timeout: -1     # 启动失败不阻塞
      minimum-idle: 0                      # 初始 0 连接,按需建

第 6 步:CDS(Class Data Sharing)

# JDK 13+ 内置 AppCDS,把类元数据存到共享存档
# 启动时直接 mmap,跳过 class 解析

# 1. 生成 archive(一次)
$ java -Xshare:off \
       -XX:ArchiveClassesAtExit=app-cds.jsa \
       -jar app.jar
# 应用启动到完成,然后 ctrl+c

# 2. 启动时使用
$ java -Xshare:auto \
       -XX:SharedArchiveFile=app-cds.jsa \
       -jar app.jar

# 效果:启动时间减少 15-30%

# Spring Boot 3.3+ 集成 CDS,更方便
# 自动生成 archive 并使用
$ java -Dspring.context.exit=onRefresh \
       -XX:ArchiveClassesAtExit=app.jsa \
       -jar app.jar

# 后续启动
$ java -XX:SharedArchiveFile=app.jsa -jar app.jar

# Dockerfile 集成
FROM eclipse-temurin:17-jdk AS builder
WORKDIR /app
COPY app.jar app.jar
RUN java -Dspring.context.exit=onRefresh \
         -XX:ArchiveClassesAtExit=app.jsa \
         -jar app.jar || true

FROM eclipse-temurin:17-jre
COPY --from=builder /app/app.jar /app/app.jar
COPY --from=builder /app/app.jsa /app/app.jsa
ENTRYPOINT ["java", "-XX:SharedArchiveFile=/app/app.jsa", "-jar", "/app/app.jar"]

第 7 步:Spring AOT(GraalVM Native)

<!-- 启动从秒级到毫秒级,但有限制 -->
<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                </plugin>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

# 编译(慢,要 3-5 分钟)
$ mvn -Pnative native:compile

# 运行
$ ./target/app
Started Application in 0.234 seconds      # 234ms!

# 限制:
# - 反射、动态代理要显式声明
# - ClassLoader 玩法受限(MyBatis、Hibernate 等要适配)
# - 编译产物大(150MB+)
# - 内存占用反而高(初期)

# 适用场景:Serverless / 短生命周期 / CLI 工具
# 不适合:复杂业务系统(改造成本高)

本地开发优化(devtools)

<!-- 本地用 devtools 热重载 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

<!-- IDEA 自动编译 + devtools 热重载,改完 Java 文件 2 秒生效 -->
<!-- 注意:生产必须 spring.devtools.restart.enabled=false -->

# JRebel(收费,效果更好,改完立即生效)
# 适合大型项目本地开发

K8s 部署优化

apiVersion: apps/v1
kind: Deployment
metadata:
  name: product
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 0     # 不允许减少可用 Pod
  template:
    spec:
      containers:
      - name: app
        # 启动慢的服务:用 startupProbe,给足时间
        startupProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          failureThreshold: 30
          periodSeconds: 3
        # liveness 启动后才生效
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: 8080
          periodSeconds: 10
        # 优雅关闭:50 秒让 in-flight 请求完成
        lifecycle:
          preStop:
            exec:
              command: ["sleep", "20"]
      terminationGracePeriodSeconds: 60

优化效果对比

阶段                启动时间       jar 大小    内存
=====================================================
原始                 60s            80MB       1.4GB
+ 关无用 AutoConfig  52s            80MB       1.4GB
+ 剪冗余依赖         50s            50MB       1.2GB
+ Bean 懒加载        35s            50MB       1.1GB
+ 并行初始化         30s            50MB       1.1GB
+ AppCDS             15s            50MB       1.1GB
+ Spring AOT (native) 0.25s         50MB       300MB

最终上线选择:CDS + Lazy + 并行,8 秒
原因:Spring AOT 改造成本太高(反射注解全要重写)
       8 秒已经够好

业务影响:
- K8s 滚动 20 Pod 从 20min 缩到 3min
- 故障恢复秒级,业务报错率降 90%
- HPA 扩容 50 Pod 从 1h 缩到 10min
- 本地开发体验大幅提升

避坑清单

  1. 先测量再优化(spring-startup-analyzer / actuator/startup)
  2. 剪无用 AutoConfiguration,classpath 越干净启动越快
  3. spring.main.lazy-initialization=true 慎用,核心 Bean 排除
  4. 独立初始化任务并行执行(CompletableFuture)
  5. JDK 13+ 必上 AppCDS,无侵入大幅提速
  6. HikariCP minimum-idle=0,启动不阻塞建连接
  7. K8s 用 startupProbe,不要 livenessProbe 误杀启动中的 Pod
  8. Spring AOT 适合 Serverless,大型业务系统改造成本高
  9. 本地用 devtools 热重载,生产关掉
  10. 启动后做 JIT 预热(跑一遍核心路径)

总结

Spring Boot 启动优化是个层层挖掘的过程:从 60 秒到 8 秒,每一步都有具体的工具和方法。最大的认知改变:不要一上来就上 Spring Native,改造成本高、限制多,先把 CDS + Lazy + 并行用满,90% 项目就够了。被低估的是 AppCDS,JDK 自带、无侵入、效果立竿见影,Spring Boot 3.3 还内置了集成,2024 年还没上的项目都该补上。最容易踩坑的是 Bean 懒加载,DataSource、ServletWebServer 这种核心 Bean 必须排除在懒加载之外,否则第一个请求会卡 5-10 秒。最后,启动慢往往是依赖管理问题:120 个 jar 里真正用的可能只有 60 个,定期 mvn dependency:tree 审视,剪掉冗余,classpath 干净了启动自然快。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

MySQL 主从延迟 25 分钟事故复盘:并行复制 + ProxySQL 工程治理

2026-5-19 13:05:16

技术教程

TypeScript monorepo 编译 12min 优化到 90s:Project Refs + SWC + Turbo

2026-5-19 13:09:24

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索