想获取更多高质量的Java技术文章?欢迎访问Java技术小馆官网,持续更新优质内容,助力技术成长
Java技术小馆官网https://www.yuque.com/jtostring
SpringBoot 为何启动慢?
某天你刚写完一个 SpringBoot 接口,点击运行后盯着控制台发呆:"怎么还卡在 Tomcat started on port 8080?这启动速度也太慢了吧!" 此时你脑海中浮现出面试官的灵魂拷问:"SpringBoot 为什么启动慢?"
但真相是:当你在开发环境看到 "Started Application in 5.2 seconds" 时,实际上 SpringBoot 已经处于 "满载(Full Load)" 状态。 这个状态下的启动时间,和你在生产环境中看到的启动时间可能天差地别!
一、SpringBoot 启动慢?先看这三个关键数据
案例:一个普通项目的启动时间线
2023-08-01 10:00:00.000 INFO [main] o.s.b.StartupInfoLogger : Starting Application
2023-08-01 10:00:00.500 INFO [main] o.s.c.s.ClassPathXmlApplicationContext : Refreshing...
2023-08-01 10:00:03.200 INFO [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2023-08-01 10:00:04.100 INFO [main] o.s.b.w.e.tomcat.TomcatWebServer : Tomcat started on port 8080
2023-08-01 10:00:05.200 INFO [main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService
2023-08-01 10:00:05.250 INFO [main] o.s.b.StartupInfoLogger : Started Application in 5.25 seconds
看起来耗时 5.25 秒?但其中有三个关键阶段:
- Classpath 扫描(0-0.5s)
- Bean 初始化(0.5-4.1s)
- 满载状态(4.1-5.25s)
开发环境 vs 生产环境实测对比
环境 |
启动时间 |
已加载 Bean 数量 |
线程池状态 |
开发环境 |
5.2s |
150+ |
完整初始化 |
生产环境 |
2.1s |
80 |
延迟加载 |
结论:开发环境中的 "慢启动" 其实是满载状态的表现!
二、深挖 "满载" 的本质
1. SpringBoot 启动的三个阶段
public class SpringApplication {
// 核心启动流程
public ConfigurableApplicationContext run(String... args) {
// 阶段1:环境准备(约20%时间)
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 阶段2:上下文加载(约50%时间)
ConfigurableApplicationContext context = createApplicationContext();
refreshContext(context);
// 阶段3:满载阶段(约30%时间)
afterRefresh(context, applicationArguments);
stopWatch.stop();
// 输出 Started Application in xxx seconds
}
}
2. 开发环境为何更慢?
开发环境的特殊配置:
# application-dev.properties
spring.devtools.restart.enabled=true # 热部署
spring.jpa.show-sql=true # 显示SQL
management.endpoints.web.exposure.include=* # Actuator全开
这些配置会导致:
- 多加载 20% 的监控 Bean
- 增加 15% 的类路径扫描
- 初始化调试用线程池
3. 满载的三大特征
// 1. 所有@Bean方法已执行
@Bean
public DataSource dataSource() { // 此时已初始化完成
return new HikariDataSource();
}
// 2. 所有CommandLineRunner已运行
@Component
public class InitRunner implements CommandLineRunner {
@Override
public void run(String... args) { // 该方法已执行
// 初始化业务数据
}
}
// 3. Tomcat线程池就绪
tomcat.getConnector().getExecutor() // 返回非空线程池
三、你的项目真的慢吗?
方法1:使用 Actuator 的启动时间端点
# application.yml
management:
endpoints:
web:
exposure:
include: startup
请求 /actuator/startup
返回:
{
"springBootVersion": "3.1.2",
"timelines": {
"spring.beans.instantiate": {
"startTime": "2023-08-01T10:00:00.500Z",
"endTime": "2023-08-01T10:00:03.200Z",
"duration": "PT2.7S"
},
"tomcat.start": {
"startTime": "2023-08-01T10:00:03.200Z",
"endTime": "2023-08-01T10:00:04.100Z",
"duration": "PT0.9S"
}
}
}
方法2:Bean 加载时间排序
@Autowired
private ApplicationContext context;
public void printBeanInitTimes() {
((AbstractApplicationContext) context)
.getBeanFactory()
.getBeanDefinitionNames()
.stream()
.map(name -> new AbstractMap.SimpleEntry<>(
name,
((RootBeanDefinition) context.getBeanDefinition(name))
.getResourceDescription()))
.sorted((e1, e2) -> Long.compare(
getInitTime(e1.getKey()),
getInitTime(e2.getKey())))
.forEach(e -> System.out.println(e.getKey() + " : " + getInitTime(e.getKey())));
}
输出示例:
myDataSource : 1200ms
entityManagerFactory : 800ms
transactionManager : 400ms
四、让启动速度提升 300%
方案1:延迟加载(实测减少40%时间)
# application.properties
spring.main.lazy-initialization=true # 全局延迟加载
// 或针对特定Bean
@Lazy
@Bean
public MyHeavyBean heavyBean() { ... }
方案2:砍掉不必要的自动配置
// 手动排除自动配置类
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
HibernateJpaAutoConfiguration.class
})
// 或使用条件注解
@Configuration
@ConditionalOnProperty(name = "app.feature.cache.enabled")
public class CacheAutoConfiguration { ... }
方案3:线程池延迟初始化
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(0); // 初始0线程
executor.setMaxPoolSize(20);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.initialize(); // 首次任务提交时初始化
return executor;
}
方案4:生产环境预热(Docker 实测效果)
# Dockerfile
FROM openjdk:17-jdk-slim
COPY target/app.jar /app.jar
# 预热命令(不暴露端口)
RUN java -Dserver.port=-1 -jar /app.jar --spring.main.lazy-initialization=true &
sleep 30 && \
pkill -f 'java.*app.jar'
# 正式启动
CMD ["java", "-jar", "/app.jar"]
五、三个黄金法则
法则1:区分环境配置
# application-prod.properties
spring.main.lazy-initialization=true
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
spring.devtools.restart.enabled=false
法则2:监控先行,优化在后
推荐工具:
- SpringBoot Actuator(内置监控)
- Java Flight Recorder(JVM 级分析)
- Arthas(动态诊断)
法则3:接受合理的启动时间
不同场景的合理启动时间:
场景 |
可接受时间 |
Serverless 函数 |
<1s |
微服务实例 |
5-10s |
传统单体应用 |
20-30s |
数据分析批处理任务 |
1-5min |
六、GraalVM 原生镜像的降维打击
一个简单的对比测试:
# 传统JAR启动
java -jar app.jar → 4.1s
# 原生镜像启动
./app → 0.05s
实现步骤:
- 添加 GraalVM 依赖
<dependency>
<groupId>org.graalvm.nativeimage</groupId>
<artifactId>native-image-maven-plugin</artifactId>
<version>22.3.1</version>
</dependency>
- 构建原生镜像
mvn -Pnative native:compile