Tavio's blog Tavio's blog
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Tavio Zhang

努力学习的小码喽
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • JVM整体架构
    • 一、JVM的整体架构
    • 二、类加载子系统:从字节码到可执行类
      • 2.1 类加载的完整流程
      • 2.2 双亲委派模型:类加载的安全保障
      • 2.2.1 类加载器的层次结构
      • 2.2.2 双亲委派的执行流程
      • 2.2.3 双亲委派的核心作用
      • 2.3 打破双亲委派模型的场景
    • 三、运行时数据区:JVM的内存核心
      • 3.1 线程私有区域(随线程创建/销毁)
      • 3.2 线程共享区域(随JVM启动/销毁)
    • 四、执行引擎:字节码的“执行者”
    • 五、本地方法接口:Java与底层的桥梁
    • 六、垃圾回收:自动内存管理
      • 6.1 垃圾判定算法:可达性分析
      • 6.2 垃圾收集算法
      • 6.3 常见垃圾收集器
    • 七、JVM运行流程:从代码到执行的完整链路
    • 总结
  • JVM String Table
  • JVM GC
  • JVM 对象的创建与调用
  • 《JVM》笔记
Tavio
2022-02-13
目录

JVM整体架构

无论是Java源代码(.java)还是编译后的字节码文件(.class),都无法直接被操作系统执行。Java之所以能实现“一次编译,到处运行”,核心在于Java虚拟机(JVM)——它作为字节码与底层操作系统之间的中间层,屏蔽了不同硬件和操作系统的差异,同时自动管理内存(如垃圾回收),降低了开发者的内存管理成本。

# 一、JVM的整体架构

JVM的整体架构可划分为五大核心模块,各模块协同工作完成Java程序的运行:

┌─────────────────────────────────────────────────┐
│                  JVM 整体架构                   │
├─────────────┬─────────────────────┬─────────────┤
│  类加载子系统  │     运行时数据区     │   执行引擎   │
├─────────────┼─────────────────────┼─────────────┤
│  本地方法接口  │                   │   垃圾回收   │
└─────────────┴─────────────────────┴─────────────┘
1
2
3
4
5
6
7

各模块的核心作用:

  • 类加载子系统:将.class字节码文件加载为JVM可识别的类对象;
  • 运行时数据区:存储程序运行时的所有数据(如对象实例、局部变量等),是内存管理的核心;
  • 执行引擎:解析并执行字节码指令,是程序运行的“动力源”;
  • 本地方法接口:衔接Java代码与非Java语言(如C/C++)编写的本地方法,弥补Java底层能力不足;
  • 垃圾回收(GC):自动回收运行时数据区中无用的对象/类,避免内存泄漏和溢出。

# 二、类加载子系统:从字节码到可执行类

类加载子系统的核心任务是将.class字节码文件转换为JVM可直接使用的Class对象。类加载采用“按需加载”策略——仅当程序首次使用某个类时(如创建实例、调用静态方法),才会触发加载流程。

# 2.1 类加载的完整流程

类加载过程分为5个阶段,依次执行且不可逆:

  1. 加载(Loading)
    类加载器根据类的全限定名(如java.lang.String),通过类路径(classpath)查找对应的.class文件,将字节流读入内存,并将静态存储结构(如字段、方法、常量池)转换为运行时数据结构(存储在方法区/元空间),最终生成一个代表该类的Class对象(作为方法区中类信息的访问入口)。

  2. 校验(Verification)
    对字节流进行合法性校验,确保其符合JVM规范且不会危害虚拟机安全,是“沙箱安全”的重要保障。具体包括:

    • 文件格式校验(如是否以魔数0xCAFEBABE开头、版本号是否兼容);
    • 元数据校验(如类是否有父类、是否实现了接口的所有方法);
    • 字节码校验(确保指令逻辑合法,如不会跳转到方法体外);
    • 符号引用校验(如引用的类/方法是否存在)。
  3. 准备(Preparation)
    在元空间为类的静态变量分配内存,并赋予默认初始值(非显式赋值)。例如:

    • public static int num; 会被分配内存,默认值为0;
    • public static final int COUNT = 10; 因被final修饰,此处会直接赋予显式值10(而非默认值)。
  4. 解析(Resolution)
    将类中的符号引用(如java.util.List、add()方法名)转换为直接引用(内存地址)。例如:

    • 类名解析为对应Class对象的内存地址;
    • 方法名解析为方法字节码在元空间的具体地址。
  5. 初始化(Initialization)
    执行类的静态代码块和静态变量的显式赋值语句,是类加载中唯一执行用户代码的阶段。JVM会自动将静态变量显式赋值和静态代码块按顺序合并为<clinit>()方法,并保证该方法在多线程环境下仅被执行一次(其他线程阻塞等待)。
    注:接口初始化不会执行静态代码块,仅当接口中定义的静态变量被使用时才触发。

# 2.2 双亲委派模型:类加载的安全保障

类加载器加载类时,需遵循“双亲委派模型”——优先委托父加载器加载,仅当父加载器失败时才自己尝试加载。这一机制主要解决类重复加载和核心类安全问题。

# 2.2.1 类加载器的层次结构

JVM默认提供4层类加载器(自上而下):

  • 启动类加载器(Bootstrap ClassLoader)
    由C++实现,无对应的Java对象(getClassLoader()返回null),负责加载JVM核心类库(如JAVA_HOME/jre/lib下的rt.jar、charsets.jar等)。

  • 扩展类加载器(Extension ClassLoader)
    由Java实现(sun.misc.Launcher$ExtClassLoader),加载扩展类库(JAVA_HOME/jre/lib/ext目录或java.ext.dirs配置的jar包)。

  • 应用类加载器(Application ClassLoader)
    由Java实现(sun.misc.Launcher$AppClassLoader),加载应用程序classpath下的类(如项目代码、第三方依赖),是默认的类加载器。

  • 自定义类加载器
    开发者通过继承ClassLoader类实现,可自定义加载逻辑(如加载网络中的类、加密的类文件)。

# 2.2.2 双亲委派的执行流程

类加载请求 → 当前类加载器
    ↓(检查是否已加载:是则返回,否则继续)
    委派给父类加载器(递归)
    ↓
启动类加载器
    ↓(查找核心类库)
    找到 → 加载并返回
    ↓(未找到)
扩展类加载器
    ↓(查找扩展类库)
    找到 → 加载并返回
    ↓(未找到)
应用程序类加载器
    ↓(查找classpath)
    找到 → 加载并返回
    ↓(未找到)
自定义类加载器
    ↓(查找自定义范围)
    找到 → 加载并返回
    ↓(未找到)
抛出 ClassNotFoundException
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 2.2.3 双亲委派的核心作用

  1. 防止类重复加载:同一类文件被不同加载器加载会生成不同Class对象,导致类转换异常(如ClassCastException);
  2. 保障核心类安全:核心类(如java.lang.String)只能被启动类加载器加载,避免恶意代码替换核心类(如自定义java.lang.String并篡改逻辑)。

# 2.3 打破双亲委派模型的场景

双亲委派模型并非强制规范,可通过重写ClassLoader的loadClass()方法(跳过父加载器委派)打破。常见场景:

  • 热部署:需在JVM不重启的情况下替换类(如Spring Boot DevTools),通过自定义类加载器加载新类文件,替换旧Class对象;
  • Tomcat类加载:Tomcat为每个Web应用创建独立的WebAppClassLoader,优先加载WEB-INF/classes和WEB-INF/lib下的类(而非委派给应用类加载器),避免多应用间的类冲突(如不同应用依赖同一类的不同版本)。

# 三、运行时数据区:JVM的内存核心

类加载完成后,程序运行时的所有数据均存储在“运行时数据区”。根据是否线程共享,可分为线程私有区域和线程共享区域。

# 3.1 线程私有区域(随线程创建/销毁)

线程私有区域的生命周期与线程一致,无需GC管理。

  1. 程序计数器(Program Counter Register)

    • 作用:记录当前线程正在执行的字节码指令地址(如行号);
    • 特点:
      • 多线程切换时,通过程序计数器恢复线程执行位置;
      • 若线程执行本地方法(native),计数器值为undefined;
      • 是JVM中唯一不会发生OOM(内存溢出) 的区域。
  2. 虚拟机栈(VM Stack)

    • 作用:存储线程执行Java方法时的栈帧(每个方法对应一个栈帧);
    • 栈帧结构:
      • 局部变量表:存储方法的局部变量(如基本类型、对象引用),容量以Slot为单位(1个Slot可存boolean/byte/char/short/int/float/reference/returnAddress);
      • 操作数栈:执行字节码指令时的临时数据栈(如执行iadd指令时,从栈中弹出两个int相加后压回);
      • 方法返回地址:方法执行完后返回的位置(正常返回:调用者的下一条指令;异常返回:通过异常表确定);
    • 风险:递归调用过深或方法嵌套层级过多时,会触发StackOverflowError;若虚拟机栈可动态扩展且扩展失败,会触发OutOfMemoryError。
  3. 本地方法栈(Native Method Stack)

    • 作用:与虚拟机栈类似,但为本地方法(native)服务;
    • 实现:由JVM厂商自定义(如HotSpot直接将本地方法栈与虚拟机栈合并);
    • 风险:同样可能发生StackOverflowError或OutOfMemoryError。

# 3.2 线程共享区域(随JVM启动/销毁)

线程共享区域被所有线程共享,是GC的主要回收区域。

  1. 堆(Heap)

    • 作用:存储所有对象实例和数组(即new关键字创建的对象),是JVM中最大的内存区域;
    • 分代划分(基于“分代收集”思想,不同生命周期的对象用不同回收策略):
      • 新生代(Young Generation):存放刚创建的对象(“朝生夕死”),回收频率高(Minor GC);
        • 细分为Eden区(80%)和两个Survivor区(From Survivor、To Survivor,各占10%);
        • 对象创建优先在Eden区分配,Survivor区用于存放Minor GC后存活的对象;
      • 老年代(Old Generation):存放长时间存活的对象(如多次Minor GC后仍存活)或大对象(超过阈值直接进入),回收频率低(Full GC);
    • 风险:若堆内存不足且无法扩展,会触发OutOfMemoryError: Java heap space。
  2. 元空间(Metaspace)

    • 作用:存储类信息(如类结构、方法字节码)、常量池(字符串常量池在JDK7后移至堆)、静态变量等;
    • 与永久代的区别:JDK8前使用“永久代”(属于堆的一部分),JDK8后改用元空间(使用本地内存),避免永久代内存溢出问题;
    • 风险:若元空间内存不足,会触发OutOfMemoryError: Metaspace。

# 四、执行引擎:字节码的“执行者”

执行引擎负责解析并执行.class文件中的字节码指令,是连接字节码与底层硬件的核心。Java采用“解释执行+JIT编译”的混合执行模式,兼顾启动速度和运行效率。

  1. 解释执行
    由解释器(如HotSpot的Interpreter)逐行将字节码翻译为机器码并执行,启动速度快,但执行效率低(重复代码需重复翻译)。

  2. JIT编译(Just-In-Time Compilation)
    当某段代码被频繁执行(称为“热点代码”,如高频调用的方法、循环体),JIT编译器(如HotSpot的C1、C2编译器)会将其一次性编译为机器码并缓存,后续直接执行机器码,大幅提升效率。

    • 热点代码判定:通过“方法调用计数器”和“循环回边计数器”统计执行次数;
    • 分层编译:C1(客户端编译器,编译快,优化简单)用于启动阶段,C2(服务端编译器,编译慢,优化深入)用于长期运行的热点代码。

# 五、本地方法接口:Java与底层的桥梁

Java语言的跨平台性牺牲了部分底层操作能力(如直接访问硬件、调用系统API),本地方法接口(JNI,Java Native Interface)通过以下方式弥补:

  1. 用native关键字标记需要调用本地方法的Java方法(仅声明,无实现);
  2. 通过JNI将Java方法与C/C++实现的本地方法绑定(如注册本地方法库);
  3. 执行引擎调用本地方法时,通过本地方法栈加载并执行本地代码。

常见场景:System.currentTimeMillis()(获取系统时间)、Thread.sleep()(线程休眠)等。

# 六、垃圾回收:自动内存管理

堆和元空间是线程共享区域,对象创建和类加载会持续占用内存,若不及时清理无用数据,会导致内存溢出。GC的核心是自动识别并回收无用对象/类。

# 6.1 垃圾判定算法:可达性分析

JVM通过“可达性分析”判断对象是否可回收:

  • 以GC Root为起点,向下遍历对象引用链;
  • 若对象无任何引用链连接到GC Root,则为“无用对象”,可被回收。

GC Root包括:

  • 虚拟机栈中局部变量表的对象引用;
  • 元空间中静态变量的对象引用;
  • 本地方法栈中本地方法的对象引用;
  • 活跃线程(如正在运行的线程对象)。

# 6.2 垃圾收集算法

  1. 复制算法(Copying)

    • 适用:新生代(对象存活率低);
    • 过程:将存活对象从Eden区和From Survivor区复制到To Survivor区,清空原区域;
    • 优点:无内存碎片;
    • 缺点:需预留部分内存作为复制目标,内存利用率低。
  2. 标记-清除算法(Mark-Sweep)

    • 过程:先标记所有无用对象,再统一清理;
    • 优点:无需额外内存;
    • 缺点:产生内存碎片(影响后续大对象分配),效率低(标记和清除均需遍历所有对象)。
  3. 标记-整理算法(Mark-Compact)

    • 适用:老年代(对象存活率高);
    • 过程:标记无用对象后,将存活对象向内存一端移动,再清理边界外的无用对象;
    • 优点:无内存碎片;
    • 缺点:移动对象成本高(需更新引用地址)。

# 6.3 常见垃圾收集器

  • Serial GC:单线程收集,收集时暂停所有用户线程(STW),适用于单CPU、小堆场景;
  • Parallel GC:多线程收集,注重吞吐量(运行用户代码时间/总时间),适用于后台计算;
  • CMS(Concurrent Mark Sweep):并发收集(与用户线程并行),低延迟,适用于响应时间敏感场景(如Web应用);
  • G1(Garbage-First):面向大堆,将堆划分为多个区域,优先回收垃圾多的区域,兼顾吞吐量和延迟。

# 七、JVM运行流程:从代码到执行的完整链路

以User.java程序为例,梳理JVM的完整运行流程:

  1. 编译阶段:通过javac User.java将源代码编译为User.class字节码文件(包含类信息、方法指令等);
  2. 启动JVM:执行java User,JVM初始化(创建主线程、分配运行时数据区等);
  3. 类加载:主线程执行main()方法时,首次遇到new User(),触发类加载:
    • 应用类加载器委派父加载器尝试加载User.class,最终自己在classpath中找到并加载;
    • 经过加载→校验→准备→解析→初始化,生成User类的Class对象,存入元空间;
  4. 对象创建:
    • JVM在堆的Eden区为User对象分配内存(采用指针碰撞或空闲列表策略);
    • 初始化对象属性为默认值(如int为0);
    • 设置对象头(包含类元信息指针、哈希码、GC分代年龄等);
    • 执行构造方法(<init>()),完成显式初始化,将对象引用存入虚拟机栈的局部变量表;
  5. 执行代码:
    • 执行引擎通过解释器逐行执行main()方法的字节码;
    • 若User的sayHello()方法被频繁调用(成为热点代码),JIT编译器将其编译为机器码并缓存,后续直接执行;
    • 若调用System.currentTimeMillis(),通过本地方法接口调用C++实现的本地方法;
  6. 垃圾回收:
    • 当Eden区满时,触发Minor GC,通过复制算法回收无用User对象,存活对象进入Survivor区;
    • 若对象多次存活(如年龄达15),晋升至老年代;老年代满时触发Full GC,通过标记-整理算法回收;
  7. 程序终止:main()方法执行完毕,主线程销毁,虚拟机栈、程序计数器等线程私有区域释放;JVM退出,堆和元空间内存被操作系统回收。

# 总结

JVM是Java程序运行的核心引擎,其架构设计(类加载、内存管理、执行引擎等)直接影响程序的安全性、性能和稳定性。

编辑 (opens new window)
#JVM
上次更新: 2026/01/21, 19:29:14
JVM String Table

JVM String Table→

最近更新
01
订单超时取消
01-21
02
双 Token 登录
01-21
03
长短链接跳转
01-21
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式