JVM从操作系统层面的总体启动流程
简化的高层流水线(后面我会逐步展开):
- 操作系统启动
java可执行程序(Launcher) - JVM 进程初始化(解析命令行、设置环境)
- 启动类加载子系统(Bootstrap → Extension → App)
- 加载并解析引导类(例如
java.lang.Object、java.lang.String等) - 初始化运行时数据区(方法区/Metaspace、堆、栈、本地方法栈、程序计数器)
- 解析 JAR 的
Main-Class,创建主线程并初始化System、Input/Output、Properties等 - 类的加载 → 验证 → 准备 → 解析 → 初始化(静态块/静态字段)
- 执行
main():解释器执行,热点代码由 JIT 编译器编译为本地代码(逐渐优化) - 运行时继续(线程、GC、JNI、safepoint 等机制持续工作)
- JVM 退出(main 结束或 System.exit,被 shutdown hook 管理)
下面逐步拆解每一环节。
1) Launcher(启动器)——java -jar app.jar
-
职责:这是 OS 层调用的二进制(
$JAVA_HOME/bin/java),用来解析命令行参数、设置进程环境、加载 JVM 动态库并启动 JVM 实例(通常是 HotSpot 的libjvm.so/jvm.dll)。 -
解决的问题:桥接操作系统和 JVM 的原生实现;负责把用户命令(如
-Xmx,-D系列系统属性,-jar等)传入 JVM。 -
细节:
-jar app.jar:Launcher 会打开 jar,读取META-INF/MANIFEST.MF的Main-Class属性,作为程序入口。- 如果有
-cp和-jar同时出现,-jar优先,jar 内的 classpath 控制类加载。
-
观察方式:
java -version检查使用的 JVM 实现/版本。
2) JVM 进程初始化(解析命令行、设置环境)
- 职责:JVM 内部完成命令参数解析(堆大小、GC 策略、JIT 参数等)、初始化 JIT/GC 子系统的默认配置、设置默认类库路径、初始化日志与诊断接口。
- 解决的问题:把用户想法(内存限制、调试选项)转换成各子系统的运行参数。
- 注意:很多
-XX:参数和-X参数会影响后续模块(比如-Xmx决定堆的大小,进而影响 GC 策略)。
3) 初始化运行时数据区(Java 内存模型的主要区域)
JVM 启动时会分配/准备下面这些区域(每个区域的职责我也列出):
-
方法区 / Metaspace(Java 8+)
- 存放类的元数据(类结构、常量池、方法字节码的元信息等)。
- Java 8 之前是 PermGen,Java 8 起改为 Metaspace(native 内存中管理类元数据)。
- 解决的问题:持久保存“类的信息”,运行时需要这些信息来创建对象、解析方法调用等。
- 可调参数示例:
-XX:MaxMetaspaceSize=...
-
堆(Heap)
- 存放对象实例,GC 管理的主体区域。通常分代(年轻代 Eden + Survivor、老年代)。
- 解决的问题:对象的分配与回收(内存管理)。
- 可调参数示例:
-Xms(初始堆)、-Xmx(最大堆)、-XX:+UseG1GC等。
-
Java 栈(每线程一个)
- 存放局部变量表、操作数栈、帧数据。用于方法调用的局部信息。
- 解决的问题:方法调用状态、局部基本类型和对象引用的存储、方法返回/异常处理支持。
- 可调参数:
-Xss设置栈大小。
-
本地方法栈(Native Stack)
- 保存 JNI 本地方法调用的本地帧(在某些实现与 Java 栈合并)。
- 解决的问题:支持调用本地(C/C++)方法的运行时栈。
-
程序计数器(PC寄存器)
- 每线程保存当前字节码指令地址/下一条执行指令。
- 解决的问题:线程切换时保存执行位置(轻量小结构)。
4) 类加载子系统(ClassLoaders)——把类字节码带入运行时
-
总体职责:把
.class(来自 jar、目录、网络)字节码读进来,转换成 JVM 内部的Class对象(驻留在方法区/Metaspace)。 -
三大类加载器(常见的层次委托):
- Bootstrap ClassLoader(引导类加载器):用本地代码实现,加载 JRE 核心类(
rt.jar/ jmods 中的类,如java.lang.*)。 - Extension / Platform ClassLoader:加载扩展/平台类。
- Application / System ClassLoader:加载应用类路径下的类(jar 内的应用类通常由它加载)。
- Bootstrap ClassLoader(引导类加载器):用本地代码实现,加载 JRE 核心类(
-
双亲委派模型:
- 默认情况下,类加载器会先请求父加载器加载,父加载器找不到才由子加载器尝试加载。这样可保证核心类不会被覆盖(安全与一致性)。
-
-jar的特例:- 当使用
java -jar app.jar时,jar 内的 Class-Path 和 Main-Class 决定应用类加载器的搜索空间。
- 当使用
-
观察/调试:
-verbose:class可以打印类加载信息。
5) 类加载后的“链接”阶段:验证 → 准备 → 解析
类从字节码到可用的 Class 对象,会经过链接(linking)三个子阶段:
-
验证(Verification)
- 目的:确保字节码符合 JVM 规范,不会危害 JVM 安全(如越界访问、类型不匹配等)。
- 解决的问题:防止恶意或损坏的类破坏 VM(安全性、可靠性)。
- 内容:文件格式验证、元数据验证、字节码验证(数据流/控制流分析、操作数栈正确性)等。
-
准备(Preparation)
- 目的:为类变量分配内存并设置默认初始值(static fields)。
- 解决的问题:在初始化前建立类的静态存储位置。
-
解析(Resolution)
- 目的:把常量池中的符号引用解析为直接引用(比如把方法符号解析到具体方法) —— 这是懒解析或提前解析,取决于实现。
- 解决的问题:把符号层(名字)转换为内存地址或直接指针,便于运行时快速访问。
6) 类初始化(Initialization)
-
执行时间点:在首次主动使用类(创建实例、访问静态方法/字段、反射调用等)之前,JVM 会执行类的初始化。
-
初始化内容:
- 执行类的
<clinit>(由编译器生成,包含静态变量赋值和static块的内容)。 - 按照严格的语义保证初始化的线程安全(只有一个线程执行
<clinit>,其他线程会阻塞或看到初始化结果)。
- 执行类的
-
解决的问题:保证静态变量与静态块的正确、可见、顺序执行(Java 内存模型语义)。
7) 启动主线程与 java.lang.System / I/O 初始化
-
创建主线程(main thread):JVM 创建第一个 Java 线程并在其中调用
Main-Class的public static void main(String[])。 -
System 初始化:
- 初始化
System.out/err/in、Properties(包括user.dir、java.home等系统属性),并把args传入main。 - 如果有 SecurityManager(过去常用,近年来不常用/被弃用),会进行安全策略初始化。
- 初始化
-
解决的问题:建立 Java 程序入口,连接标准 I/O,与系统环境做衔接。
8) 执行引擎:解释器 + JIT(即时编译器)
-
解释器(Interpreter)
- 字节码逐条解释执行,启动速度快但性能较低。
- 解决问题:快速启动、低延迟的代码可执行性。
-
JIT 编译器(Just-In-Time)
- 热点代码(被频繁执行的方法/循环)会被编译成本地机器码以加速执行。
- HotSpot 常见有 Tiered Compilation(解释器 → C1(快速编译)→ C2(高优化)),也可能使用 Graal(在某些发行版中)。
- 优化示例:内联(method inlining)、逃逸分析(escape analysis,栈上分配/标量替换)、循环优化、逃逸消除、锁消除/偏向锁等。
- 解决的问题:把长期运行的 Java 代码性能接近手写本地代码,同时保持安全性与可移植性。
-
观察/调试:
-XX:+PrintCompilation、-XX:+PrintInlining,以及-XX:+UnlockDiagnosticVMOptions系列可以打印编译与内联信息。
9) 运行时服务与关键机制(持续运行阶段)
这些机制在 JVM 运行过程中持续运作:
-
垃圾回收器(Garbage Collector)
- 负责回收堆中的无用对象。常见算法:Serial、Parallel、CMS(较老)、G1(默认在很多 JDK 版本中)、ZGC、Shenandoah(低停顿 GC)。
- 工作机制:分代回收(年轻代频繁回收、老年代少量回收),并发/并行/暂停策略各异。
- 解决的问题:自动内存管理,避免内存泄露和野指针问题,同时努力降低停顿时间(GC 暂停影响应用响应)。
- 观察:
-Xlog:gc*(Java 9+)或-XX:+PrintGCDetails(早期)来打印 GC 日志。
-
safepoint(安全点)
- 为了进行如 GC、类重定义、线程栈扫描等全局操作,JVM 需要在所有线程处于可暂停状态(safepoint)。JIT 编译插入检查来使线程到达 safepoint。
- 解决的问题:在一致的状态下安全执行全局操作。
-
Deoptimization(去优化)与 On-Stack Replacement(OSR)
- 当 JIT 在优化时基于某些假设(比如类未被子类化)做了优化,如果假设被破坏(运行时出现新的子类),JVM 可以撤销优化(deopt)并返回解释执行。
- OSR 允许在方法内部循环热点处直接替换解释器为编译代码、或反向操作以保证语义一致。
- 解决的问题:动态类型系统中保持安全性的同时尽量优化性能。
-
JNI(Java Native Interface)
- Java 与本地 C/C++ 代码交互的接口。加载本地库、调用本地方法。
- 注意:本地代码可以绕过 Java 的安全与内存管理,需要谨慎(容易导致崩溃)。
-
类数据共享(CDS / AppCDS)
- 提前把常用类元数据打包共享,缩短启动时间并降低内存占用。
- 观察:
-Xshare:...相关参数。
10) JVM 退出(Shutdown)
-
触发条件:
main线程结束且没有其他非守护线程,或System.exit()被调用,或收到进程结束信号。
-
关闭流程:
- 执行注册的 shutdown hooks(
Runtime.addShutdownHook(...))并等待其完成(有超时/顺序)。 - 执行
finalize()(Java 9 起被弃用/不推荐,且行为不可靠)。 - 释放资源、卸载本地库、退出进程返回给 OS。
- 执行注册的 shutdown hooks(
-
注意:shutdown hook 的滥用会导致进程退出变慢或死锁。
常见观察与调试命令(实战)
-verbose:class:打印类的加载情况。-XX:+PrintCompilation:打印哪些方法被 JIT 编译了。-Xlog:gc*或-XX:+PrintGCDetails+-XX:+PrintGCTimeStamps:查看 GC 行为。jcmd <pid> VM.system_properties/jinfo/jmap/jstack/jstat:JVM 运行时诊断工具。-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining:查看内联决策。
把每个组件“解决的问题”再归纳(便于记忆)
- Launcher:把 OS 命令转成 JVM 配置并加载 JVM(进程启动桥接)。
- ClassLoader:把类字节码带入运行时并实现模块化/隔离。
- Verifier(验证器):保证字节码安全与规范。
- Linker(准备/解析):为类分配内存并解析符号引用(加速运行时访问)。
- Initializer(初始化):执行静态初始化,保证语义与线程安全。
- Heap + GC:管理对象的分配与回收(自动内存管理,减少内存错误)。
- Stack / PC / Frames:管理方法调用和局部变量(程序执行的临时状态)。
- Interpreter + JIT:提供可执行性与性能:解释器保证可运行,JIT 提高长期运行性能。
- JNI / Native:与平台/系统资源互操作(但更危险)。
- Safepoint / Deopt / OSR:保证运行时可以安全执行全局操作并在动态场景下恢复正确性。
常见误解与陷阱
- “JVM 没有垃圾回收,所以内存泄漏只是代码问题”:不是。内存泄漏在 Java 中通常表现为对象被意外持有引用,导致 GC 无法回收。JVM 的 GC 机制并不能替你避免逻辑上的泄漏。
- “JIT 总是越晚越好”:JIT 编译提升了性能,但编译本身有成本(CPU/time)。Tiered 编译在折衷“启动速度 vs 稳定高性能”之间取平衡。
- “PermGen 仍然存在”:Java 8+ 已弃用 PermGen,采用 Metaspace(native 内存)——这会影响类加载与内存监控策略。
- “所有锁优化都透明且无风险”:锁消除/偏向锁能提高性能,但在极端多线程竞争下可能被撤销;理解 safepoint/trap 语义有助于 debug。
总结(关键记忆点)
- JVM 启动:Launcher → JVM 初始化 → 内存区分配 → 类加载(验证/准备/解析/初始化)→ 创建主线程 & 执行 main → 解释 + JIT 优化 → GC / Thread / JNI 等持续工作 → 退出。
- 每个组件的目标都是在安全、可移植的前提下提供高性能的 Java 运行环境:类加载保证模块化与安全,验证保证字节码安全,GC 管理内存,JIT 把热点变快。
