Spring 第一次启动执行慢,从 JVM 角度讲讲?

#Java#面试八股

Spring 第一次启动执行慢,从 JVM 角度讲讲?

结论先行:首次启动慢,JVM 侧的原因可以归成三类:类加载与验证(成千上万个类要读、验、解析)、解释执行与 JIT 未预热(Spring 启动期大量反射与字节码操作跑在解释器上)、CPU 与内存的资源竞争(JIT 编译线程、GC 初始化、元空间扩容)。第二次启动变快,往往只是操作系统页缓存与 CDS 存档命中的功劳,并不代表代码变快了。

一、启动时间都花在哪

阶段典型占比JVM 侧原因
JVM 初始化3%~8%堆初始化、GC 线程创建、JIT 编译器初始化、随机数种子
类加载与验证20%~35%读 class、校验字节码、解析常量池,Spring 动辄加载上万类
Bean 定义扫描20%~40%反射读注解、ASM 解析 class、getDeclaredMethods
单例实例化15%~30%反射调用构造器、CGLIB 生成代理类、元空间增长
端口与外部依赖视情况数据库连接池、注册中心、配置中心首次握手
启动时间线:
0s ─ JVM 初始化 ─ 类加载 ─ 注解扫描 ─ Bean 创建 ─ 上下文刷新 ─ 就绪
        │            │          │            │
        │            │          │            └─ CGLIB 生成代理:新类 → 元空间扩容
        │            │          └─ 反射/ASM:全在解释执行,未达 JIT 阈值
        │            └─ 验证 + 解析:每个类都要走一遍
        └─ 首次启动无 CDS 缓存,页缓存也是冷的

二、能直接量化的证据

# 1. 看类加载耗时:统计 ClassLoad 事件数
java -Xlog:class+load=info -jar app.jar 2>&1 | grep -c "source:"

# 2. 看启动期有多少方法被编译(一般极少,说明大部分在解释执行)
java -XX:+PrintCompilation -jar app.jar 2>&1 | head -30

# 3. 看各阶段耗时(Spring Boot 自带)
java -jar app.jar --debug 2>&1 | grep -E "Started .* in "

# 4. 启动期的 GC 与元空间
java -Xlog:gc*,gc+metaspace=info -jar app.jar 2>&1 | head -20

三、四个见效最快的调整

# ① CDS / AppCDS:把类加载与验证结果存档,二次启动直接映射
java -XX:ArchiveClassesAtExit=app.jsa -jar app.jar        # 首次生成
java -XX:SharedArchiveFile=app.jsa -jar app.jar           # 后续启动

# ② 启动期降级编译:只到 C1,省掉 C2 的编译开销(短生命周期进程适用)
java -XX:TieredStopAtLevel=1 -jar app.jar

# ③ 固定堆、限制 JIT 线程,避免与业务线程抢 CPU
java -Xms2g -Xmx2g -XX:CICompilerCount=2 -jar app.jar

# ④ 容器里给足 CPU 与熵
java -Djava.security.egd=file:/dev/./urandom -XX:ActiveProcessorCount=4 -jar app.jar

TieredStopAtLevel=1 在长驻服务上不推荐,它牺牲峰值性能换启动速度,只适合 CLI、函数计算、批处理。

四、启动耗时该量到什么粒度

指标采集方式参考基线(中型服务)
JVM 启动到 main 入口-Xlog:init 或用包装脚本打时间戳0.2~1s
类加载总数-Xlog:class+load=info 计数8000~20000 个
类加载总耗时JFR ClassLoad 事件1~4s
首个 Bean 就绪Spring --debug 的启动报告视规模
启动期 GC 次数-Xlog:gc 计数0~2 次
元空间最终占用jcmd VM.metaspace80~250MB

先有基线再谈优化。把上面六个数记进发布检查单,下一次「启动变慢」才有的比,而不是凭感觉。

五、常见追问

追问回答要点
为什么第二次启动快很多操作系统页缓存命中 + CDS 存档命中,类加载的磁盘 I/O 与验证开销大幅下降
启动慢一定是 JVM 问题吗不,数据库连接池初始化、注册中心超时、DNS 解析、随机数熵不足都更常见
元空间为什么会拖慢启动大量动态代理类触发元空间扩容,每次扩容可能伴随 Full GC
Spring 6 有什么改善AOT 处理 + GraalVM native image,把 Bean 定义在构建期算好,启动进入百毫秒级
怎么定位到具体 Bean--debug 看条件评估报告;或用 JFR 的 ExecutionSample 抓启动期热点方法

小结

首启慢是「类加载 + 解释执行 + 资源竞争」三件事叠加的结果,CDS、编译层级、CPU 配额是三条最快的优化路径。判断是否值得优化,先看启动耗时是否落在扩缩容与发布的关键路径上。

笔记加载中…