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
- 本地开发体验大幅提升
避坑清单
- 先测量再优化(spring-startup-analyzer / actuator/startup)
- 剪无用 AutoConfiguration,classpath 越干净启动越快
- spring.main.lazy-initialization=true 慎用,核心 Bean 排除
- 独立初始化任务并行执行(CompletableFuture)
- JDK 13+ 必上 AppCDS,无侵入大幅提速
- HikariCP minimum-idle=0,启动不阻塞建连接
- K8s 用 startupProbe,不要 livenessProbe 误杀启动中的 Pod
- Spring AOT 适合 Serverless,大型业务系统改造成本高
- 本地用 devtools 热重载,生产关掉
- 启动后做 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