JVM 启动流程
理解 JVM 的启动流程对于排查启动问题、优化启动时间以及深入理解 JVM 工作原理都非常重要。本章将详细讲解 JVM 从命令行执行到程序运行准备就绪的完整过程。
JVM 启动概述
当你执行 java -jar myapp.jar 时,背后发生了一系列复杂的操作。JVM 启动流程可以大致分为以下几个阶段:
各阶段说明:
- 命令行解析:解析 JVM 参数和应用程序参数
- JVM 初始化:创建 JVM 实例,初始化内存、线程等
- 类加载:加载主类及其依赖类
- 主类执行:执行 main 方法
- 程序运行:程序正常运行阶段
详细启动流程
第一阶段:命令行解析
当你在命令行执行 java 命令时,JVM 首先会解析命令行参数。
参数分类详解:
标准参数(Standard Options):所有 JVM 实现都必须支持的参数。
# 常见标准参数
-version # 显示版本信息
-classpath # 设置类路径
-D<name>=<value> # 设置系统属性
-jar # 执行 JAR 文件
非标准参数(Non-Standard Options):以 -X 开头,不保证所有 JVM 实现都支持。
# 常见非标准参数
-Xmx<size> # 设置最大堆大小
-Xms<size> # 设置初始堆大小
-Xss<size> # 设置线程栈大小
-Xmn<size> # 设置新生代大小
高级选项(Advanced Options):以 -XX: 开头,用于调优和诊断。
# 布尔类型选项
-XX:+UseG1GC # 启用 G1 收集器
-XX:+PrintGCDetails # 打印 GC 详情
# 数值类型选项
-XX:MaxHeapSize=4g # 设置最大堆大小
-XX:NewRatio=2 # 设置新生代与老年代比例
# 字符串类型选项
-XX:HeapDumpPath=/tmp/heap.hprof # 堆转储路径
参数解析顺序:
JVM 按照特定顺序处理参数:
- 首先处理
.hotspotrc文件(如果存在) - 然后处理
JAVA_TOOL_OPTIONS环境变量 - 接着处理命令行参数
- 最后处理
_JAVA_OPTIONS环境变量
第二阶段:JVM 初始化
完成命令行解析后,JVM 开始初始化。这是启动过程中最复杂的阶段。
创建 JVM 实例
JVM 的入口是 JNI_CreateJavaVM 函数,它会创建 JVM 实例并初始化各个子系统。
初始化的关键组件:
-
内存管理子系统
- 分配堆内存
- 初始化方法区(元空间)
- 创建代码缓存
-
线程系统
- 创建主线程
- 创建 VM 线程(执行 VM 内部操作)
- 创建 GC 线程(后台垃圾回收)
-
类加载器系统
- 创建启动类加载器(Bootstrap Class Loader)
- 创建扩展类加载器(Extension Class Loader)
- 创建应用类加载器(Application Class Loader)
-
执行引擎
- 初始化解释器
- 初始化 JIT 编译器
内存初始化详解
JVM 在启动时需要分配多种内存区域:
内存分配顺序:
- 预留虚拟地址空间:JVM 首先预留一大块虚拟地址空间(取决于
-Xmx) - 提交初始内存:实际分配初始大小的物理内存(取决于
-Xms) - 初始化各区域:划分新生代、老年代、元空间等
相关参数:
# 堆内存
-Xms2g # 初始堆大小
-Xmx2g # 最大堆大小
# 元空间
-XX:MetaspaceSize=128m # 初始元空间大小
-XX:MaxMetaspaceSize=512m # 最大元空间大小
# 代码缓存
-XX:InitialCodeCacheSize=160k # 初始代码缓存
-XX:ReservedCodeCacheSize=240m # 最大代码缓存
# 线程栈
-Xss1m # 每个线程的栈大小
# 直接内存
-XX:MaxDirectMemorySize=256m # 最大直接内存
预初始化类
JVM 启动时,会预先加载和初始化一些核心类:
// JVM 内部预初始化的核心类
java.lang.Object // 所有类的父类
java.lang.String // 字符串类
java.lang.Class // 类的运行时表示
java.lang.Thread // 线程类
java.lang.Throwable // 异常基类
java.lang.reflect.* // 反射相关类
java.util.* // 常用工具类
第三阶段:类加载
JVM 初始化完成后,开始加载用户指定的主类。
主类加载流程
加载过程的三个阶段:
1. 加载(Loading)
找到 .class 文件并读取内容:
- 从文件系统读取
- 从 JAR 文件读取
- 从网络读取
- 动态生成(如动态代理)
2. 链接(Linking)
将类的二进制数据合并到 JVM 运行时环境中:
- 验证:确保
.class文件符合 JVM 规范,不会危害 JVM - 准备:为静态变量分配内存并设置默认初始值
- 解析:将符号引用转换为直接引用
3. 初始化(Initialization)
执行类的初始化代码:
- 执行静态初始化块
- 初始化静态变量
public class MainClass {
// 准备阶段:staticVar = 0(默认值)
// 初始化阶段:staticVar = 100(显式赋值)
private static int staticVar = 100;
// 初始化阶段执行
static {
System.out.println("类初始化中...");
}
public static void main(String[] args) {
System.out.println("主方法执行");
}
}
第四阶段:主类执行
主类加载完成后,JVM 会调用 main 方法。
main 方法调用
main 方法要求:
// 正确的 main 方法签名
public static void main(String[] args) { }
// 以下变体也是合法的
static public void main(String[] args) { } // public 和 static 顺序可变
public static void main(String... args) { } // 可变参数
public static void main(String args[]) { } // 数组声明位置可变
主线程启动:
// JVM 内部的伪代码
public class Launcher {
public static void launch(Class<?> mainClass, String[] args) {
// 1. 创建主线程
Thread mainThread = new Thread("main");
// 2. 设置线程上下文类加载器
mainThread.setContextClassLoader(ClassLoader.getSystemClassLoader());
// 3. 调用 main 方法
Method mainMethod = mainClass.getMethod("main", String[].class);
mainMethod.invoke(null, (Object) args);
}
}
第五阶段:程序运行
main 方法开始执行后,程序进入正常运行阶段。此时 JVM 的各个子系统已经准备就绪。
JVM 运行时状态
关键运行时组件:
- JIT 编译器:在后台将热点代码编译为本地代码
- 垃圾回收器:自动回收不再使用的对象
- 类加载器:按需加载新的类
- 线程调度器:调度线程执行
启动优化技巧
理解 JVM 启动流程后,我们可以针对性地优化启动性能。
1. 类数据共享(Class Data Sharing, CDS)
CDS 允许多个 JVM 进程共享类的元数据,减少启动时间和内存占用。
# JDK 9+:自动生成默认 CDS 归档
java -Xshare:dump
# 使用 CDS 启动应用
java -Xshare:on -jar myapp.jar
# 生成应用特定的 CDS 归档
java -XX:ArchiveClassesAtExit=app.jsa -jar myapp.jar
# 使用应用 CDS 归档启动
java -XX:SharedArchiveFile=app.jsa -jar myapp.jar
CDS 工作原理:
2. 应用类数据共享(AppCDS)
AppCDS 扩展了 CDS,支持应用类的共享:
# 步骤1:生成类列表
java -Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=app.lst -jar myapp.jar
# 步骤2:生成共享归档
java -Xshare:dump -XX:+UseAppCDS -XX:SharedClassListFile=app.lst -XX:SharedArchiveFile=app.jsa -cp myapp.jar
# 步骤3:使用共享归档启动
java -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=app.jsa -jar myapp.jar
3. 分层编译优化
调整分层编译策略可以加快启动速度:
# 禁用分层编译,只使用 C1 编译器
-XX:-TieredCompilation -XX:TieredStopAtLevel=1
# 这会降低峰值性能,但能显著加快启动速度
4. 内存预分配
使用 AlwaysPreTouch 在启动时预分配所有内存:
# 启动时预分配内存,避免运行时延迟
-Xms4g -Xmx4g -XX:+AlwaysPreTouch
优缺点分析:
| 参数 | 优点 | 缺点 |
|---|---|---|
+AlwaysPreTouch | 消除运行时内存分配延迟 | 增加启动时间 |
-Xms=Xmx | 避免堆扩容开销 | 可能浪费内存 |
TieredStopAtLevel=1 | 加快启动 | 降低峰值性能 |
5. 使用 GraalVM 原生镜像
对于对启动时间要求极高的场景,可以使用 GraalVM 原生镜像:
# 构建原生镜像
native-image -jar myapp.jar
# 执行原生镜像(毫秒级启动)
./myapp
启动时间对比:
启动问题排查
启动失败常见原因
1. 类找不到
# 错误信息
Error: Could not find or load main class com.example.Main
# 排查步骤
# 1. 检查类路径设置
echo %CLASSPATH%
# 2. 检查类名是否正确(区分大小写)
java com.example.Main # 正确
java com.example.main # 错误
# 3. 检查 package 声明是否正确
2. 内存不足
# 错误信息
Error occurred during initialization of VM
Could not reserve enough space for object heap
# 排查步骤
# 1. 检查系统可用内存
# 2. 降低 -Xmx 设置
# 3. 检查是否有其他内存密集型进程
# 解决方案
-Xmx1g # 降低最大堆大小
3. 版本不兼容
# 错误信息
Unsupported major.minor version 52.0
# 原因:代码用高版本 JDK 编译,在低版本 JVM 上运行
# 排查
java -version # 检查运行时版本
javap -verbose MyClass | findstr "major version" # 检查类文件版本
4. 参数错误
# 错误信息
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
Unrecognized option: -XX:InvalidOption
# 排查步骤
# 1. 检查参数拼写
# 2. 检查参数是否适用于当前 JVM 版本
# 3. 使用 -XshowSettings:all 查看所有设置
java -XshowSettings:all -version
启动诊断工具
使用 JVM 启动日志:
# 打印详细的 JVM 初始化信息
-XX:+PrintFlagsFinal # 打印所有 JVM 参数
-Xlog:init+redefine=info # 打印初始化信息(JDK 9+)
使用 jinfo 查看运行时配置:
# 查看 JVM 参数
jinfo -flags <pid>
# 查看系统属性
jinfo -sysprops <pid>
使用 Java Flight Recorder 分析启动:
# 在启动时开始录制
java -XX:StartFlightRecording=duration=60s,filename=startup.jfr -jar myapp.jar
# 使用 JDK Mission Control 分析 startup.jfr
# 可以看到类加载、方法编译等详细信息
JVM 生命周期
JVM 关闭流程
当程序结束或被终止时,JVM 会执行关闭流程:
Shutdown Hook 使用:
public class ShutdownHookExample {
public static void main(String[] args) {
// 注册关闭钩子
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("执行清理操作...");
// 关闭数据库连接
// 刷新缓存
// 释放资源
}));
System.out.println("程序运行中...");
// 程序正常结束或被终止时,会执行关闭钩子
}
}
关闭钩子的特点:
- 程序正常退出时执行
- 调用
System.exit()时执行 - 用户按 Ctrl+C 时执行
- 系统关闭时执行
- 可以注册多个钩子,并发执行
注意:如果 JVM 崩溃或被 kill -9 强制终止,关闭钩子不会执行。
小结
JVM 启动流程是一个复杂但有序的过程:
- 命令行解析:解析 JVM 参数和应用程序参数
- JVM 初始化:创建 JVM 实例,初始化内存、线程、类加载器等
- 类加载:加载主类及其依赖
- 主类执行:调用 main 方法
- 程序运行:JIT 编译、垃圾回收等后台工作
理解启动流程有助于:
- 排查启动失败问题
- 优化启动性能
- 深入理解 JVM 工作原理