我们的一个内部工具服务,部署在 Serverless 上,每次冷启动要 8 秒。函数计费按运行时长,8 秒里一半在"加载类、初始化 Spring 上下文",用户已经走了。老板问能不能压到 2 秒内,于是有了这次冷启动专项优化。
先量化,再动手
不量就优化是瞎猜。我用 -Xlog:class+load:file=classload.log 和 Spring 的 startup 报告,把 8 秒拆开:
| 阶段 | 耗时 |
|---|---|
| JVM 启动 + 基础类加载 | 1.1s |
| Spring 上下文初始化 | 3.4s |
| 业务 Bean 实例化与连接建立 | 2.8s |
| 首次请求编译预热(JIT) | 0.7s |
大头在 Spring 上下文和业务连接。冷启动优化的本质,是把"启动时必须做"和"用时再做"分开。
类加载优化:CDS 先把类归档
每次启动都重新加载上万类,重复劳动。JDK 的 CDS(Class Data Sharing)可以把已加载的类归档,下次直接内存映射:
# 第一步:训练出一份归档(跑一次正常启动)
java -Xshare:dump -XX:ArchiveClassesAtExit=app.jsa -jar app.jar
# 第二步:启动时用归档
java -Xshare:on -XX:SharedArchiveFile=app.jsa -jar app.jar
类加载阶段从 1.1s 降到 0.4s。注意 Spring Boot 的 fat jar 里类在嵌套 jar 中,需要先解压或用 --layer 优化;我们用 spring-boot-maven-plugin 的 layered 模式分出 dependencies 和 application 两层,CDS 只对稳定的依赖层生效,命中率最高。
懒加载改造:不是所有 Bean 都要早起
Spring Boot 默认饿汉式初始化所有单例 Bean。我们用 spring.main.lazy-initialization=true 改成懒加载,启动时不实例化,首次用到才建。这一项让 Spring 上下文从 3.4s 降到 1.2s。但懒加载有副作用:配置错误从"启动即报错"变成"运行时才炸",我们补了 @EventListener(ContextRefreshedEvent) 的关键 Bean 自检,确保核心链路提前初始化。
连接建立的延迟
2.8s 里有 1.9s 是建 Redis、MySQL、MQ 连接。这些在 Serverless 冷启动时是必须预热否则首次请求会卡。折中:把"默认连接池最小空闲数"设为 1,启动时只建最小连接;其余连接随流量懒建。再配合连接池预热——启动末尾主动发一条 PING 和一次轻量查询,把握手开销前置到启动期,用户请求不再承担建连延迟。
依赖精简:少即是快
翻 mvn dependency:tree,发现引了 spring-boot-starter-web 却只用 REST 调用(不用内嵌 Tomcat),换 spring-boot-starter-webflux + 响应式客户端反而更重。最终精简掉 11 个间接依赖,jar 从 78MB 降到 41MB,类数量少了一大截,CDS 归档更小、加载更快。一个典型的多余引入:
<!-- 去掉,我们根本没用模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
成果与代价
最终冷启动:8.0s → 1.5s,提速 81%。代价是懒加载让"配置错"暴露更晚,我们靠启动自检和灰度发布兜住;CDS 归档要在每次依赖变更后重新生成,进了 CI 脚本自动处理。Serverless 账单上,单次调用均摊启动成本从 8 秒降到 1.5 秒,每月省下约 600 元,不算多,但用户体验的投诉少了一大半。
先到这
《Java 应用冷启动优化:从 8 秒到 1.5 秒》这块我前前后后踩了不止一次。今天先写这些,后面想到新的再补。