Skip to content

JVM 架构详解

JVM 是什么、为什么需要?

  定义: JVM(Java Virtual Machine)是JAVA虚拟计算机。

  在没有 JVM 的时代,C/C++ 程序需要针对不同操作系统和 CPU 架构分别编译,同一份源码在 Windows 上编译出的 exe 无法在 Linux 上运行,在 x86 上编译的二进制无法在 ARM 上运行。Sun 公司的 James Gosling 团队在设计 Java 时提出了一个核心理念:(一次编写,到处运行)。因此JVM 就是为了解决这个跨平台问题而诞生的。它在操作系统和 Java 程序之间引入了一个抽象层。

如下图示:

一次编写,到处运行

  Java程序运行的时候,编译器(Javac.exe)将.java文件编译成平台无关的字节码文件(.class), JVM对字节码文件进行解释,翻译成对应平台匹配的机器指令并运行。

  JVM负责运行.class,只要能翻译成符合规范的.class文件,都能被JVM运行。

  JVM是一个跨平台的运行环境,有自己的生态系统。

  从宏观角度,JVM它定义了指令集、寄存器集、类文件结构、堆栈、垃圾回收堆和内存区域等规范。JVM 规范本身不规定实现细节,因此有了不同的 JVM 实现。

主流 JVM 实现

实现开发商特点
HotSpot VMOracle / OpenJDK最主流,混合模式解释+编译
OpenJ9IBM / Eclipse低内存占用,快速启动
GraalVMOracle多语言运行时,AOT 编译
Zing VMAzulC4 垃圾回收器,超低延迟

JVM 的核心价值

价值说明
跨平台字节码一次编译,任意 JVM 运行
内存管理自动垃圾回收,无需手动 free/delete
安全性沙箱机制,字节码校验,防止恶意代码
性能优化运行时 JIT 编译,热点代码优化
生态基础Kotlin、Scala、Groovy 等语言均运行在 JVM 上

JVM 整体架构

JVM 架构可以分为三大子系统 + 两大核心组件:

+---------------------------------------------------------------+
|                        JVM 架构总览                            |
|                                                               |
|  +------------------+  +------------------+  +------------------+
|  |   类加载器子系统   |  |   运行时数据区     |  |    执行引擎        |
|  |   ClassLoader    |  |   Runtime Data   |  |  Execution Engine|
|  |   Subsystem      |  |   Areas          |  |                   |
|  |                  |  |                  |  |                   |
|  |  Bootstrap       |  |  +-----------+  |  |  解释器 (Interpreter)|
|  |  +-----------+   |  |  | 方法区     |  |  |  JIT 编译器 (C1/C2) |
|  |  | Extension |   |  |  | (Method   |  |  |  垃圾回收器 (GC)    |
|  |  +-----------+   |  |  |  Area)    |  |  |                   |
|  |  | Application|   |  |  +-----------+  |  +------------------+
|  |  +-----------+   |  |  | 堆 (Heap)  |  |
|  |  | Custom    |   |  |  +-----------+  |
|  |  +-----------+   |  |  | 栈 (Stack) |  |
|  +------------------+  |  +-----------+  |
|                        |  | 程序计数器  |  |
|                        |  | (PC Register)| |
|                        |  +-----------+   |
|                        |  | 本地方法栈  |  |
|                        |  | (Native    |  |
|                        |  |  Method    |  |
|                        |  |  Stack)    |  |
|                        |  +-----------+   |
|                        +------------------+
+---------------------------------------------------------------+
            |                                          |
            v                                          v
+------------------+                        +------------------+
|   本地方法接口     |                        |   本地方法库        |
|   JNI (Java      |----------------------->|   Native Method   |
|   Native         |                        |   Libraries       |
|   Interface)     |                        |                   |
+------------------+                        +------------------+

类加载器子系统(ClassLoader Subsystem)

负责加载 .class 文件到内存。类加载器分为三层:

  • Bootstrap ClassLoader:加载 <JAVA_HOME>/lib 下的核心类库(rt.jar, java.* 等),由 C++ 实现
  • Extension ClassLoader(JDK 9+ 改为 Platform ClassLoader):加载 <JAVA_HOME>/lib/ext 下的扩展类库
  • Application ClassLoader:加载 classpath 下的应用类

运行时数据区(Runtime Data Areas)

JVM 在运行 Java 程序时会使用多种内存区域:

区域线程共享存放内容生命周期
堆(Heap)所有对象实例和数组JVM 启动到结束
方法区(Method Area)类信息、常量、静态变量、JIT 代码缓存JVM 启动到结束
虚拟机栈(VM Stack)栈帧(局部变量表、操作数栈、动态链接、返回地址)与线程同生灭
本地方法栈(Native Stack)本地方法调用与线程同生灭
程序计数器(PC Register)当前线程执行字节码的行号指示器与线程同生灭

JDK 8 方法区变化: 方法区在 HotSpot 中的实现是"永久代"(PermGen),JDK 8 移除永久代,改用本地内存中的"元空间"(Metaspace)。

执行引擎(Execution Engine)

负责执行字节码。核心组件:

  • 解释器(Interpreter):逐条读取字节码解释执行,启动快但运行慢
  • JIT 编译器(Just-In-Time Compiler):将热点代码编译为本地机器码,编译慢但运行快
  • 垃圾回收器(GC):自动回收不再使用的对象

HotSpot JVM 深入

HotSpot 是 OpenJDK 的默认 JVM 实现,名称来源于其"热点代码探测"机制。

解释执行 vs 编译执行

程序启动阶段:
  .class 字节码 --> 解释器逐条执行 --> 运行结果
                          |
                     检测热点代码
                          |
                          v
                JIT 编译器编译为本地机器码
                          |
                          v
              后续调用直接执行本地机器码(极快)

解释执行:逐条将字节码翻译为机器码执行,每次执行都需要翻译,但不需要等待编译时间。

编译执行:将字节码一次性编译为本地机器码,首次编译有开销,但后续执行直接使用机器码,效率极高。

JIT 编译器:C1 和 C2

HotSpot 内置两个 JIT 编译器:

编译器场景特点
C1(Client Compiler)桌面应用编译速度快,优化程度较低,适合对启动时间敏感的场景
C2(Server Compiler)服务端应用编译速度慢,但优化程度高,运行时性能更好

分层编译(Tiered Compilation):JDK 7 开始引入,是 C1 和 C2 的混合模式:

Level 0: 解释执行(不编译,收集 profiling 数据)
Level 1: C1 编译,无 profiling
Level 2: C1 编译,带基础 profiling
Level 3: C1 编译,带完整 profiling
Level 4: C2 编译,基于 profiling 数据进行深度优化

默认情况下,热点代码从 Level 0 逐步升级到 Level 4,平衡了启动速度和峰值性能。

JIT 编译优化技术

1. 方法内联(Method Inlining)

将热点方法调用替换为方法体本身,减少方法调用开销:

java
// 编译前
public int compute(int a, int b) {
    return add(a, b) * 2;
}
private int add(int x, int y) {
    return x + y;
}

// JIT 内联后(等价于)
public int compute(int a, int b) {
    return (a + b) * 2;
}

2. 逃逸分析(Escape Analysis)

分析对象的作用域,如果对象不会逃逸出方法或线程,可以进行优化:

java
// 对象不会逃逸出方法 → 可以栈上分配
public void process() {
    Point p = new Point(1, 2);  // p 不会逃逸 → 可能在栈上分配,无需 GC
    int result = p.x + p.y;
}

// 标量替换:将对象拆分为基本类型成员
// 等价于:
public void process() {
    int x = 1;
    int y = 2;
    int result = x + y;
}

3. 锁消除(Lock Elision)

java
// 同步块中的对象不会逃逸,锁可以被消除
public String concat(String a, String b) {
    StringBuffer sb = new StringBuffer();  // 局部变量,不会逃逸
    sb.append(a);                          // 内部有 synchronized
    sb.append(b);                          // 锁可以被消除
    return sb.toString();
}

4. 栈上分配(Stack Allocation)

经过逃逸分析,如果对象不会逃逸出方法,可以将其分配在栈上而非堆上,方法结束自动销毁,无需 GC 介入。

查看 JIT 编译信息

bash
# 打印编译信息
java -XX:+PrintCompilation MyApp

# 输出示例:
#  70    1       3       java.lang.String::hashCode (55 bytes)
#  71    2       3       java.lang.String::charAt (29 bytes)
#  72    3       4       java.lang.String::hashCode (55 bytes)  -- 被 C2 重新编译
  • 第一列:编译ID(毫秒时间戳)
  • 第二列:编译ID
  • 第三列:编译层级(3=C1,4=C2)
  • 后面:方法名和字节码大小

代码示例:观察 JVM 行为

1. 观察类加载

java
public class ClassLoadingDemo {
    public static void main(String[] args) {
        // 打印各个类加载器
        System.out.println("String 的类加载器: " 
            + String.class.getClassLoader());                          // null (Bootstrap)
        System.out.println("应用程序类加载器: " 
            + ClassLoadingDemo.class.getClassLoader());                // AppClassLoader
        System.out.println("应用类加载器的父加载器: " 
            + ClassLoadingDemo.class.getClassLoader().getParent());    // PlatformClassLoader
        System.out.println("平台类加载器的父加载器: " 
            + ClassLoadingDemo.class.getClassLoader().getParent().getParent()); // null
    }
}

2. 观察内存区域

java
/**
 * VM Args: -Xms20m -Xmx20m -XX:+PrintGCDetails
 */
public class MemoryAreaDemo {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        // 堆内存分配
        byte[] allocation1 = new byte[2 * _1MB];
        byte[] allocation2 = new byte[2 * _1MB];
        byte[] allocation3 = new byte[2 * _1MB];
        byte[] allocation4 = new byte[4 * _1MB];  // 触发 GC

        System.out.println("程序运行结束");
    }
}

3. 观察 JIT 优化效果

java
/**
 * VM Args: -XX:+PrintCompilation
 */
public class JITDemo {
    public static void main(String[] args) {
        for (int i = 0; i < 10_000; i++) {
            hotMethod();  // 被调用多次,触发 JIT 编译
        }
    }

    private static int hotMethod() {
        int sum = 0;
        for (int i = 0; i < 1000; i++) {
            sum += i;
        }
        return sum;
    }
}

4. 观察逃逸分析

java
/**
 * VM Args: -XX:+DoEscapeAnalysis -XX:+PrintEscapeAnalysis
 *          -XX:+EliminateAllocations
 */
public class EscapeAnalysisDemo {
    public static void main(String[] args) {
        long start = System.nanoTime();
        for (int i = 0; i < 100_000_000; i++) {
            alloc();
        }
        long end = System.nanoTime();
        System.out.println("耗时: " + (end - start) / 1_000_000 + " ms");
    }

    private static void alloc() {
        User user = new User();
        user.id = 1;
        user.name = "test";
        // user 对象不会逃逸出 alloc() 方法
        // 开启逃逸分析后,可能被栈上分配甚至标量替换
    }

    static class User {
        int id;
        String name;
    }
}

对比开启/关闭逃逸分析:

bash
# 开启逃逸分析(默认)
java -XX:+DoEscapeAnalysis EscapeAnalysisDemo

# 关闭逃逸分析
java -XX:-DoEscapeAnalysis EscapeAnalysisDemo

常用 JVM 参数

参数说明
-Xms初始堆大小
-Xmx最大堆大小
-Xss每个线程栈大小
-XX:MetaspaceSize元空间初始大小
-XX:MaxMetaspaceSize元空间最大大小
-XX:+PrintCompilation打印 JIT 编译信息
-XX:+PrintGCDetails打印 GC 详细信息
-XX:+UseG1GC使用 G1 垃圾回收器
-XX:+UseZGC使用 ZGC 垃圾回收器
-XX:+DoEscapeAnalysis开启逃逸分析(默认开启)
-XX:+TieredCompilation开启分层编译(默认开启)

面试高频问题

Q1: JVM 是如何实现"一次编写,到处运行"的?

A: Java 源码被 javac 编译为平台无关的字节码(.class 文件),字节码不包含任何平台相关的指令。JVM 充当了字节码和操作系统之间的桥梁,每种操作系统都有对应的 JVM 实现,JVM 负责将字节码翻译为当前平台的机器码执行,从而实现了跨平台。

Q2: JVM、JRE、JDK 有什么区别?

A: JVM 是 Java 虚拟机,负责执行字节码。JRE = JVM + 核心类库 + 其他运行支持,是 Java 运行时环境。JDK = JRE + 开发工具(javac, javap, jdb 等),是 Java 开发工具包。

Q3: 方法区(Method Area)和永久代(PermGen)是一回事吗?

A: 不是。方法区是 JVM 规范中的逻辑概念,永久代是 HotSpot 对方法区的一种实现。JDK 8 中永久代被移除,HotSpot 改用本地内存中的"元空间"(Metaspace)来实现方法区。元空间的好处是使用本地内存,默认不设置上限,不易发生 OOM。

Q4: 解释执行和 JIT 编译的区别是什么?

A: 解释执行是逐条翻译字节码为机器码执行,启动快但运行慢。JIT 编译是将热点代码一次性编译为本地机器码,编译有开销但后续执行极快。HotSpot 采用混合模式:启动时解释执行,探测到热点代码后触发 JIT 编译,实现"快速启动 + 高峰性能"的平衡。

Q5: 什么是分层编译?

A: 分层编译是 HotSpot 从 JDK 7 开始引入的优化策略。它将编译分为 5 个层级(Level 0-4):Level 0 是解释执行,Level 1-3 是 C1 编译,Level 4 是 C2 编译。热点代码逐步升级,C1 快速编译保证启动速度,C2 深度优化保证峰值性能。

Q6: 逃逸分析是什么?它能带来哪些优化?

A: 逃逸分析是 JIT 编译器的一种优化技术,分析对象的作用域。如果对象不会逃逸出方法或线程,可以进行:1)栈上分配(对象分配在栈上,方法结束自动释放);2)标量替换(将对象拆分为基本类型成员);3)锁消除(删除不必要的同步操作)。

Q7: 什么情况下 JVM 会退出?

A: JVM 在以下情况退出:1)所有非守护线程结束;2)某个线程调用 System.exit()Runtime.halt();3)遇到未捕获的异常导致主线程退出;4)操作系统强制终止进程。