跳到主要内容

JVM 启动流程

理解 JVM 的启动流程对于排查启动问题、优化启动时间以及深入理解 JVM 工作原理都非常重要。本章将详细讲解 JVM 从命令行执行到程序运行准备就绪的完整过程。

JVM 启动概述

当你执行 java -jar myapp.jar 时,背后发生了一系列复杂的操作。JVM 启动流程可以大致分为以下几个阶段:

各阶段说明

  1. 命令行解析:解析 JVM 参数和应用程序参数
  2. JVM 初始化:创建 JVM 实例,初始化内存、线程等
  3. 类加载:加载主类及其依赖类
  4. 主类执行:执行 main 方法
  5. 程序运行:程序正常运行阶段

详细启动流程

第一阶段:命令行解析

当你在命令行执行 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 按照特定顺序处理参数:

  1. 首先处理 .hotspotrc 文件(如果存在)
  2. 然后处理 JAVA_TOOL_OPTIONS 环境变量
  3. 接着处理命令行参数
  4. 最后处理 _JAVA_OPTIONS 环境变量

第二阶段:JVM 初始化

完成命令行解析后,JVM 开始初始化。这是启动过程中最复杂的阶段。

创建 JVM 实例

JVM 的入口是 JNI_CreateJavaVM 函数,它会创建 JVM 实例并初始化各个子系统。

初始化的关键组件

  1. 内存管理子系统

    • 分配堆内存
    • 初始化方法区(元空间)
    • 创建代码缓存
  2. 线程系统

    • 创建主线程
    • 创建 VM 线程(执行 VM 内部操作)
    • 创建 GC 线程(后台垃圾回收)
  3. 类加载器系统

    • 创建启动类加载器(Bootstrap Class Loader)
    • 创建扩展类加载器(Extension Class Loader)
    • 创建应用类加载器(Application Class Loader)
  4. 执行引擎

    • 初始化解释器
    • 初始化 JIT 编译器

内存初始化详解

JVM 在启动时需要分配多种内存区域:

内存分配顺序

  1. 预留虚拟地址空间:JVM 首先预留一大块虚拟地址空间(取决于 -Xmx
  2. 提交初始内存:实际分配初始大小的物理内存(取决于 -Xms
  3. 初始化各区域:划分新生代、老年代、元空间等

相关参数

# 堆内存
-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 运行时状态

关键运行时组件

  1. JIT 编译器:在后台将热点代码编译为本地代码
  2. 垃圾回收器:自动回收不再使用的对象
  3. 类加载器:按需加载新的类
  4. 线程调度器:调度线程执行

启动优化技巧

理解 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 启动流程是一个复杂但有序的过程:

  1. 命令行解析:解析 JVM 参数和应用程序参数
  2. JVM 初始化:创建 JVM 实例,初始化内存、线程、类加载器等
  3. 类加载:加载主类及其依赖
  4. 主类执行:调用 main 方法
  5. 程序运行:JIT 编译、垃圾回收等后台工作

理解启动流程有助于:

  • 排查启动失败问题
  • 优化启动性能
  • 深入理解 JVM 工作原理

参考资料