Spring Boot 启动时,JVM 是如何工作的?
场景:你执行
java -jar myapp.jar,一个 Spring Boot 应用开始启动。
我们将沿着真实启动流程,逐个拆解 JVM 五大组件的角色。
🧩 前提:Spring Boot 程序的本质
Spring Boot 应用只是一个标准的 Java 程序,它有一个 main 方法:
@SpringBootApplication
public class MyApplication {public static void main(String[] args) {SpringApplication.run(MyApplication.class, args);}
}
当你运行 java -jar myapp.jar 时,JVM 并不知道这是 Spring Boot。它只做一件事:
找到
Main-Class(比如com.example.MyApplication),加载它,初始化它,然后调用它的main()方法。
Spring Boot 的所有魔法,都是在 main() 被调用之后才开始的。
而在这之前,全是 JVM 的工作。
1️⃣ 类加载器子系统(Class Loader Subsystem)
✅ 它是什么?
JVM 中负责读取 .class 文件并转换为内部类表示的模块。
在 Spring Boot 启动时,主要由 Application ClassLoader(也叫 System ClassLoader)工作。
✅ 它做了什么?怎么做的?
- 从
myapp.jar中读取MANIFEST.MF,找到Main-Class: com.example.MyApplication; - 在 JAR 的
BOOT-INF/classes/和BOOT-INF/lib/中查找com/example/MyApplication.class; - 将字节码加载进 JVM,生成
Class<MyApplication>对象; - 同时加载 Spring Boot 相关类(如
SpringApplication,@SpringBootApplication等)。
🔍 Spring Boot 使用了自定义类加载逻辑(通过
LaunchedURLClassLoader),但底层仍基于 JVM 的类加载机制。
✅ 为什么要这么做?
- Java 是“编译一次,到处运行”,代码必须以字节码形式交给 JVM;
- JVM 必须在调用
main()前,完全理解这个类的结构(方法、注解、父类等)。
❌ 如果不这么做会怎样?
- JVM 找不到
MyApplication.class→ 报错:ClassNotFoundException; - 如果类加载器不解析注解(如
@SpringBootApplication),Spring Boot 就无法知道要开启自动配置。
💡 关键点:Spring Boot 的“自动配置”能力,依赖于 JVM 能正确加载并解析类上的注解——这是类加载器的基础能力。
2️⃣ 运行时数据区(Runtime Data Areas)
✅ 它是什么?
JVM 内部的逻辑内存模型,用于分类存储程序运行时的数据。
在 Spring Boot 启动时,主要涉及:
- 方法区(Metaspace):存类信息(如
MyApplication的结构、注解、方法字节码); - Java 虚拟机栈:为
main()方法分配栈帧(存局部变量args); - 堆(Heap):存放 Spring 容器、Bean、配置对象等(启动后大量创建)。
✅ 它做了什么?怎么做的?
- 当
MyApplication.class被加载,其元数据(类名、方法、注解)存入方法区; - 调用
main()时,JVM 在栈上创建一个栈帧,存放args参数; - 执行
SpringApplication.run(...)时,创建SpringApplication对象,存入堆; - 后续创建的 Bean(如
UserService,DataSource)也都放在堆中。
✅ 为什么要这么做?
- 分类管理:类信息几乎不变,适合放方法区;对象动态创建,适合放堆;方法调用需要隔离,适合用栈。
- 效率与安全:避免不同数据互相干扰(比如方法局部变量不会覆盖类结构)。
❌ 如果不这么做会怎样?
- 如果类信息放堆里:可能被 GC 错误回收,导致
NoClassDefFoundError; - 如果没有栈:递归调用或嵌套方法无法管理局部变量,程序逻辑混乱;
- 如果所有对象放一起:无法对“长期存活的 Spring 容器”和“临时对象”做不同 GC 策略,性能极差。
💡 Spring Boot 启动时会加载上千个类,方法区必须高效存储这些元数据,否则启动慢或 OOM。
3️⃣ 执行引擎(Execution Engine)
✅ 它是什么?
JVM 中真正执行字节码的模块。它逐条解释或编译执行 main() 及其调用的方法。
✅ 它做了什么?怎么做的?
- 从
main()的第一条字节码开始执行; - 调用
SpringApplication.run()→ 执行 Spring Boot 的启动逻辑:- 解析
@SpringBootApplication; - 扫描 Bean;
- 创建 ApplicationContext;
- 初始化内嵌 Tomcat(如果 Web 应用);
- 解析
- 所有这些动作,最终都转化为字节码指令的执行(如
invokestatic,new,putfield)。
🔍 执行引擎可能先解释执行(慢但快启动),后续对热点代码(如 Controller)JIT 编译为机器码(快)。
✅ 为什么要这么做?
- 字节码是平台无关的,但计算机只能执行本地指令;
- 执行引擎充当“翻译+执行者”,屏蔽底层差异,实现“一次编译,到处运行”。
❌ 如果不这么做会怎样?
- 字节码无法被执行,程序卡死;
- 如果没有 JIT,Spring Boot 处理 HTTP 请求会非常慢(每次都要解释字节码)。
💡 Spring Boot 的启动速度、运行性能,直接依赖执行引擎的效率。
4️⃣ 本地方法接口(JNI, Java Native Interface)
✅ 它是什么?
JVM 提供的调用操作系统本地功能(C/C++ 代码)的桥梁。
✅ 它做了什么?怎么做的?
在 Spring Boot 启动过程中,JNI 被隐式调用多次:
- 创建线程:主线程、Tomcat 工作线程 → 调用 OS 的
pthread_create(Linux); - 文件读取:加载
application.properties、读取 JAR 内资源 → 调用 OS 文件 API; - 网络绑定:Tomcat 启动时绑定 8080 端口 → 调用
socket(),bind()等系统调用; - 时间获取:日志打时间戳 → 调用
gettimeofday()。
这些操作在 Java 层通过
native方法实现,如FileInputStream.read0()。
✅ 为什么要这么做?
- Java 不能直接操作硬件或操作系统;
- 但 Spring Boot 需要文件、网络、线程、时间等基础能力;
- JNI 让 JVM 能安全地“借用”操作系统能力。
❌ 如果不这么做会怎样?
- 无法读配置文件 → Spring Boot 启动失败;
- 无法绑定端口 → Web 服务无法启动;
- 无法创建线程 → Tomcat 无法处理并发请求。
💡 没有 JNI,Java 就是一个“沙盒玩具”,Spring Boot 根本无法作为真实应用运行。
5️⃣ 垃圾回收器(Garbage Collector, GC)
✅ 它是什么?
JVM 中自动回收无用对象的模块,防止内存泄漏。
✅ 它做了什么?怎么做的?
Spring Boot 启动时:
- 创建大量临时对象:类元数据、Bean 定义、配置解析中间对象;
- GC(如 G1 或 ZGC)在后台运行:
- 识别哪些对象不再被引用(如解析完的 YAML 节点);
- 回收它们占用的堆内存;
- 启动完成后,GC 继续监控运行时对象(如 HTTP 请求产生的临时对象)。
✅ 为什么要这么做?
- Spring Boot 启动过程会瞬时创建大量对象(可达数十万);
- 如果不自动回收,堆内存迅速耗尽 →
OutOfMemoryError; - 手动管理内存(如 C++ 的
delete)在复杂框架中几乎不可能。
❌ 如果不这么做会怎样?
- 程序启动到一半就 OOM 崩溃;
- 即使启动成功,处理几个请求后内存耗尽;
- 开发者必须手动跟踪每个对象生命周期——在 Spring 这种高度动态的框架中完全不可行。
💡 GC 是 Spring Boot 能“开箱即用”的关键:开发者无需关心内存,专注业务逻辑。
🧭 总结:Spring Boot 启动时 JVM 五大组件协作全景
| 组件 | 在 Spring Boot 启动中的角色 | 关键性 |
|---|---|---|
| 类加载器 | 加载 MyApplication 和所有 Spring 类 | ⭐⭐⭐⭐⭐(没它,连 main 都找不到) |
| 运行时数据区 | 存类信息、方法栈、Bean 对象 | ⭐⭐⭐⭐(数据无处安放) |
| 执行引擎 | 执行 main() 和 Spring 初始化逻辑 | ⭐⭐⭐⭐⭐(没它,代码只是文本) |
| JNI | 提供文件、网络、线程等 OS 能力 | ⭐⭐⭐⭐(没它,Spring Boot 是“残废”) |
| GC | 自动清理启动过程中的临时对象 | ⭐⭐⭐(没它,内存迅速耗尽) |
