引言

为什么要学习这个主题?

在前两篇文章中,我们知道了Java程序如何运行,以及JVM的本质。但JVM内部到底是怎么工作的?

想象一下:

  • 一个.class文件是如何被加载到JVM中的?
  • 对象和变量存储在哪里?
  • 字节码是如何被"翻译"成机器码的?

理解JVM的架构,就像理解一台计算机的组成(CPU、内存、硬盘)一样重要。这是后续学习类加载、内存管理、GC调优的基础。

你将学到什么?

  • ✅ JVM的整体架构图
  • ✅ 五大核心组件的职责
  • ✅ 各组件如何协作运行Java程序
  • ✅ JVM的完整执行流程

一、JVM架构全景图

1.1 整体架构

┌─────────────────────────────────────────────────────────────┐
│                      Java应用程序                             │
│                   (.java → .class)                           │
└──────────────────────┬──────────────────────────────────────┘
                       │ 字节码
                       ↓
┌─────────────────────────────────────────────────────────────┐
│                      JVM(Java虚拟机)                        │
│                                                               │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ 1️⃣ 类加载子系统 (Class Loader Subsystem)              │ │
│  │  - 加载 (Loading)                                      │ │
│  │  - 链接 (Linking): 验证、准备、解析                    │ │
│  │  - 初始化 (Initialization)                             │ │
│  └────────────────────────────────────────────────────────┘ │
│                       ↓                                      │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ 2️⃣ 运行时数据区 (Runtime Data Areas)                  │ │
│  │                                                          │ │
│  │  【线程共享】                 【线程私有】              │ │
│  │  - 堆 (Heap)                 - 程序计数器 (PC Register)│ │
│  │  - 方法区 (Method Area)      - 虚拟机栈 (VM Stack)    │ │
│  │                              - 本地方法栈 (Native Stack)│ │
│  └────────────────────────────────────────────────────────┘ │
│                       ↓                                      │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ 3️⃣ 执行引擎 (Execution Engine)                         │ │
│  │  - 解释器 (Interpreter)                                │ │
│  │  - JIT编译器 (Just-In-Time Compiler)                   │ │
│  │  - 垃圾收集器 (Garbage Collector)                      │ │
│  └────────────────────────────────────────────────────────┘ │
│                       ↓                                      │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ 4️⃣ 本地接口 (Native Interface - JNI)                  │ │
│  └────────────────────────────────────────────────────────┘ │
│                       ↓                                      │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ 5️⃣ 本地方法库 (Native Method Libraries)               │ │
│  └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
                       ↓
┌─────────────────────────────────────────────────────────────┐
│                    操作系统 & 硬件                            │
└─────────────────────────────────────────────────────────────┘

1.2 核心组件概览

组件作用类比
类加载子系统加载、链接、初始化类快递员(把包裹送到仓库)
运行时数据区存储数据(对象、变量、代码)内存/仓库
执行引擎执行字节码CPU(执行指令)
本地接口调用操作系统API操作系统接口
垃圾收集器自动回收无用对象清洁工(清理垃圾)

二、类加载子系统(Class Loader Subsystem)

2.1 职责

负责将.class文件加载到JVM内存中,并准备好供使用。

2.2 三个阶段

.class文件
    ↓
1️⃣ Loading(加载)
    ↓
2️⃣ Linking(链接)
    - Verification(验证)
    - Preparation(准备)
    - Resolution(解析)
    ↓
3️⃣ Initialization(初始化)
    ↓
运行时数据区(可以使用了)

加载(Loading)

动作

  • 通过类的全限定名找到.class文件
  • 读取字节码数据
  • 在堆中生成一个Class对象

示例

// 触发加载
Class<?> clazz = Class.forName("com.example.User");

链接(Linking)

1. 验证(Verification)

  • 检查字节码是否符合JVM规范
  • 防止恶意代码

2. 准备(Preparation)

  • 为静态变量分配内存
  • 设置默认值

示例

public class Example {
    private static int count = 100;  // 准备阶段:count = 0
                                     // 初始化阶段:count = 100
}

3. 解析(Resolution)

  • 将符号引用转换为直接引用
  • 符号引用:字符串形式(如"com.example.User")
  • 直接引用:内存地址

初始化(Initialization)

动作

  • 执行类构造器<clinit>()方法
  • 执行静态变量赋值
  • 执行静态代码块

示例

public class Example {
    private static int count = 100;  // ← 此时执行

    static {
        System.out.println("静态代码块执行");  // ← 此时执行
    }
}

2.3 类加载器层次

┌────────────────────────┐
│ 启动类加载器           │  加载核心类库(rt.jar)
│ Bootstrap ClassLoader  │  如:java.lang.*, java.util.*
└──────────┬─────────────┘
           │ 父加载器
┌──────────▼─────────────┐
│ 扩展类加载器           │  加载扩展库(ext目录)
│ Extension ClassLoader  │  如:javax.*
└──────────┬─────────────┘
           │ 父加载器
┌──────────▼─────────────┐
│ 应用类加载器           │  加载应用类(classpath)
│ Application ClassLoader│  如:自己写的类
└────────────────────────┘

双亲委派模型

  • 先让父加载器尝试加载
  • 父加载器无法加载时,子加载器才尝试
  • 保证核心类不会被篡改

后续章节会详细讲解,这里先了解即可。


三、运行时数据区(Runtime Data Areas)

3.1 整体布局

┌─────────────────────────────────────────────┐
│           运行时数据区                       │
│                                             │
│  ┌────────────────────┐  ┌────────────────┐│
│  │  方法区 (Method)   │  │  堆 (Heap)     ││  【线程共享】
│  │  - 类信息          │  │  - 对象实例    ││  所有线程都能访问
│  │  - 静态变量        │  │  - 数组        ││
│  │  - 常量池          │  │                ││
│  └────────────────────┘  └────────────────┘│
│                                             │
│  ┌──────────┐ ┌──────────┐ ┌──────────────┐│
│  │程序计数器│ │虚拟机栈   │ │本地方法栈    ││  【线程私有】
│  │PC Register│ │VM Stack  │ │Native Stack ││  每个线程独有
│  └──────────┘ └──────────┘ └──────────────┘│
└─────────────────────────────────────────────┘

3.2 各区域详解

程序计数器(Program Counter Register)

作用:记录当前线程执行到哪条字节码指令

特点

  • 最小的内存区域
  • 线程私有
  • 唯一不会OOM的区域

类比:书签(记录读到第几页)

虚拟机栈(VM Stack)

作用:存储方法执行时的局部变量、操作数栈、方法出口等

结构

┌─────────────────┐
│   栈帧3(栈顶)  │  ← 当前正在执行的方法
├─────────────────┤
│   栈帧2          │  ← 调用者
├─────────────────┤
│   栈帧1          │  ← main方法
└─────────────────┘

每个栈帧包含

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法返回地址

异常

  • StackOverflowError:递归调用过深
  • OutOfMemoryError:无法分配新栈

本地方法栈(Native Method Stack)

作用:为Native方法服务(用C/C++写的方法)

示例

// JDK中的native方法
public native int hashCode();

堆(Heap)

作用:存储对象实例和数组

特点

  • 最大的内存区域
  • 所有线程共享
  • 垃圾收集器的主要管理区域

结构(简化版):

┌───────────────────────────┐
│       新生代 (Young)       │  新创建的对象
│  - Eden                   │
│  - Survivor 0             │
│  - Survivor 1             │
├───────────────────────────┤
│       老年代 (Old)         │  长期存活的对象
└───────────────────────────┘

示例

User user = new User();  // user变量在栈,User对象在堆

方法区(Method Area)

作用:存储类信息、静态变量、常量池

存储内容

  • 类的结构信息(字段、方法、构造器)
  • 运行时常量池
  • 静态变量

演变

  • JDK 7:永久代(PermGen)
  • JDK 8+:元空间(Metaspace)

区别

永久代:在堆内存中,有大小限制
元空间:在本地内存中,默认无限制(受操作系统限制)

四、执行引擎(Execution Engine)

4.1 职责

将字节码转换为机器码并执行。

4.2 三种执行方式

字节码
    ↓
┌───┴────────────────┐
│                    │
▼                    ▼
解释执行          JIT编译
(逐行翻译)        (一次编译,缓存)
    ↓                ↓
机器码            机器码
    ↓                ↓
  CPU执行          CPU执行

解释器(Interpreter)

特点

  • 逐行解释执行字节码
  • 启动快,执行慢
  • 适合初次执行的代码

JIT编译器(Just-In-Time Compiler)

特点

  • 将热点代码编译成机器码
  • 启动慢,执行快
  • 适合频繁执行的代码

HotSpot的JIT

  • C1编译器:客户端编译器,编译速度快
  • C2编译器:服务端编译器,优化程度深

分层编译(Tiered Compilation):

0层:解释执行
    ↓
1层:C1编译(简单优化)
    ↓
2层:C1编译(带profiling)
    ↓
3层:C1编译(完全优化)
    ↓
4层:C2编译(深度优化)

垃圾收集器(Garbage Collector)

作用:自动回收堆中无用的对象

后续会有专门章节详细讲解。


五、本地接口 & 本地方法库

5.1 本地接口(JNI - Java Native Interface)

作用:让Java代码调用C/C++代码(或被调用)

使用场景

  • 调用操作系统API
  • 复用现有C/C++库
  • 性能关键的代码

示例

public class NativeDemo {
    // 声明native方法
    public native void hello();

    static {
        // 加载动态库
        System.loadLibrary("native-lib");
    }
}

5.2 本地方法库

包含

  • C/C++编写的动态库(.so、.dll、.dylib)
  • JVM底层实现(文件IO、网络、线程等)

六、完整执行流程示例

6.1 代码示例

public class Example {
    private static int count = 10;

    public static void main(String[] args) {
        User user = new User("Alice");
        user.sayHello();
    }
}

class User {
    private String name;

    public User(String name) {
        this.name = name;
    }

    public void sayHello() {
        System.out.println("Hello, " + name);
    }
}

6.2 执行流程

1️⃣ 类加载阶段
   - 加载Example.class到方法区
   - 加载User.class到方法区
   - 初始化:count = 10

2️⃣ main方法执行
   - 在虚拟机栈创建main方法的栈帧
   - PC寄存器指向main方法的第一条指令

3️⃣ 创建User对象
   User user = new User("Alice");

   ① 在堆中分配User对象的内存
   ② 初始化对象(调用构造器)
   ③ 将堆中对象的地址赋值给栈中的user变量

   【内存布局】
   栈:user变量 → 0x1234(堆地址)
   堆:0x1234 → User对象{name: "Alice"}

4️⃣ 调用方法
   user.sayHello();

   ① 在虚拟机栈创建sayHello方法的栈帧
   ② 执行方法体(字节码)
   ③ 解释器执行或JIT编译执行
   ④ 调用System.out.println(native方法)
   ⑤ 通过JNI调用操作系统API
   ⑥ 输出到控制台

5️⃣ 方法返回
   - 弹出sayHello栈帧
   - 弹出main栈帧
   - 程序结束

6️⃣ 垃圾回收
   - User对象不再被引用
   - GC自动回收堆中的对象

七、实战:观察JVM运行时数据

7.1 查看堆内存使用情况

public class MemoryInfo {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();

        // 获取内存信息(单位:字节)
        long maxMemory = runtime.maxMemory();      // 最大堆内存
        long totalMemory = runtime.totalMemory();  // 已分配堆内存
        long freeMemory = runtime.freeMemory();    // 空闲堆内存

        System.out.println("最大堆内存: " + (maxMemory / 1024 / 1024) + "MB");
        System.out.println("已分配堆内存: " + (totalMemory / 1024 / 1024) + "MB");
        System.out.println("空闲堆内存: " + (freeMemory / 1024 / 1024) + "MB");
        System.out.println("已使用堆内存: " +
            ((totalMemory - freeMemory) / 1024 / 1024) + "MB");
    }
}

输出示例

最大堆内存: 4096MB
已分配堆内存: 256MB
空闲堆内存: 200MB
已使用堆内存: 56MB

7.2 模拟StackOverflowError

public class StackOverflowDemo {
    private static int depth = 0;

    public static void recursion() {
        depth++;
        recursion();  // 无限递归
    }

    public static void main(String[] args) {
        try {
            recursion();
        } catch (StackOverflowError e) {
            System.out.println("栈深度: " + depth);
            e.printStackTrace();
        }
    }
}

输出

栈深度: 15532
java.lang.StackOverflowError

说明:每次方法调用都会在栈上创建栈帧,递归过深导致栈溢出。


总结

核心要点回顾

  1. JVM五大核心组件

    • 类加载子系统:加载、链接、初始化类
    • 运行时数据区:存储数据的内存结构
    • 执行引擎:执行字节码(解释 + JIT)
    • 本地接口:调用C/C++代码
    • 垃圾收集器:自动回收对象
  2. 运行时数据区

    • 线程共享:堆、方法区
    • 线程私有:程序计数器、虚拟机栈、本地方法栈
  3. 执行流程

    • 类加载 → 内存分配 → 方法执行 → 垃圾回收
  4. 关键概念

    • 栈:存储方法调用和局部变量
    • 堆:存储对象实例
    • 方法区:存储类信息

思考题

  1. 为什么局部变量存储在栈中,而对象存储在堆中?
  2. 如果一个方法内创建了100万个对象,会导致StackOverflowError还是OutOfMemoryError?
  3. 静态变量存储在哪里?为什么?

下一篇预告

下一篇《字节码是什么?从.java到.class的编译过程》,我们将深入探讨:

  • 字节码的格式和结构
  • javac编译器的工作原理
  • 常量池、方法表、字节码指令
  • 如何手工解读字节码

参考资料


欢迎在评论区分享你对JVM架构的理解!