当前位置: 首页 > web >正文

深度解析【JVM】三大核心架构:运行时数据区、类加载与垃圾回收机制


目录

1.前言

插播一条消息~

2.正文

2.1JVM运行流程

2.2JVM运行时数据区

2.2.1堆(线程共享)

2.2.2栈(线程私有)

2.2.3本地方法栈(线程私有)

2.2.4程序计数器(线程私有)

2.2.5方法区(线程共享)

2.2.6内存布局中的异常问题(堆溢出和栈溢出)

2.3JVM类加载

2.3.1类加载过程

2.3.1.1加载

2.3.1.2链接

2.3.1.3初始化

2.3.2双亲委派模型

2.3.2.1模型结构

2.3.2.2核心加载器

2.3.2.3工作流程

2.3.2.4破坏双亲委派的场景

2.3.2.5总结

2.4垃圾回收GC

2.4.1死亡对象的判断算法

2.4.1.1引用计数法

2.4.1.2可达性分析法

2.4.2垃圾回收算法

2.4.2.1标记-清除法

2.4.2.2复制法

2.4.2.3标记-整理法

2.4.2.4分代算法

2.4.3垃圾收集器

3.小结


1.前言

作为Java生态的基石,Java虚拟机(JVM)承担着字节码解释执行、内存自动管理和跨平台兼容的核心职责。理解JVM工作机制,不仅是解决OutOfMemoryErrorStackOverflowError等生产环境问题的关键,更是高级开发者必须掌握的系统级知识。

本文将系统剖析以下核心模块:

  1. 执行流程:从.class字节码到机器指令的转化流程

  2. 内存迷宫:堆/栈/方法区的数据存储结构与线程安全设计

  3. 类加载机制:双亲委派模型的安全保障与破坏场景

  4. 垃圾回收体系:从对象存活判定算法到收集器演进

带您深度剖析JVM,结合代码实例与内存快照,揭示JVM底层运作逻辑。


插播一条消息~

🔍十年经验淬炼 · 系统化AI学习平台推荐

系统化学习AI平台https://www.captainbed.cn/scy/

📚 完整知识体系:从数学基础 → 工业级项目(人脸识别/自动驾驶/GANs),内容由浅入深
💻 实战为王:每小节配套可运行代码案例(提供完整源码)
🎯 零基础友好:用生活案例讲解算法,无需担心数学/编程基础

🚀 特别适合

  • 想系统补强AI知识的开发者
  • 转型人工智能领域的从业者
  • 需要项目经验的学生

2.正文

2.1JVM运行流程

Java程序的执行依赖于Java虚拟机(JVM)。理解JVM的核心运行流程是掌握Java底层机制的基础。整个过程可以清晰地划分为以下几个关键阶段:

1.源码编译为字节码:

  • .java 源代码文件被 javac 编译器处理。
  • 生成平台无关的 字节码 (.class 文件),这是JVM的“机器语言”,实现“一次编写,到处运行”。

2.启动JVM:

  • 用户执行命令(如 java MyApp)。
  • 操作系统创建 JVM进程 并初始化运行环境。

3.类加载:

  • JVM的 类加载器 查找并加载程序所需的 .class 文件(主类及其依赖)。
  • 加载过程包含关键步骤:
  1. 加载: 读取字节码。
  2. 验证: 确保字节码安全合规。
  3. 准备: 为类的静态变量分配内存并赋默认值。
  4. 解析: (可稍后)将符号引用转换为直接引用。
  5. 初始化: 执行静态代码块,为静态变量赋程序设定的初始值。

4.内存分配(运行时数据区):

  • JVM划分内存区域管理运行数据:
  1. 方法区: 存储类元信息、常量池、静态变量(JDK 8+ 使用元空间)。
  2. 堆: 存储所有对象实例和数组,是垃圾回收(GC) 的主战场。
  3. 虚拟机栈 (线程私有): 由栈帧组成,存储方法调用的局部变量、操作数栈等信息。
  4. 程序计数器 (线程私有): 记录当前线程执行的字节码指令地址。
  5. 本地方法栈 (线程私有): 支持调用本地方法(如C/C++代码)。

5.执行字节码:

  • 执行引擎 负责执行加载的字节码:
  1. 解释器: 逐条解释执行字节码(启动快,效率较低)。
  2. 即时编译器 (JIT): 将高频执行的代码(热点代码) 动态编译成本地机器码,大幅提升执行效率。
  • 垃圾收集器 (GC): 自动回收堆中不再使用的对象(“垃圾”)所占用的内存。
  • 本地方法接口 (JNI): 提供Java代码与本地方法(如C/C++库)互调的标准机制。

6.程序执行与结束:

  • 执行引擎找到并调用 main 方法(程序入口)。
  • 程序逻辑运行:创建对象、调用方法、执行计算等。
  • 程序结束条件:
  1. main 方法正常结束。
  2. 调用 System.exit()
  3. 未捕获的异常/错误导致线程终止(若无其他非守护线程,则JVM退出)。
  4. 操作系统强制终止JVM进程。

图解: 


2.2JVM运行时数据区


2.2.1堆(线程共享)

核心作用:存储所有对象实例数组
特性

  1. 生命周期:随JVM启动创建,退出销毁。

  2. 内存管理:由垃圾收集器(GC)自动回收(主战场)。

  3. 分代设计(优化GC效率):

    • 新生代(Young Generation)

      • Eden区(对象诞生地)

      • Survivor区(From + To,存活对象过渡区)

    • 老年代(Old Generation):长期存活对象

    • 元数据区(Metaspace,JDK8+):替代永久代(PermGen),存储类元信息

  4. 异常OutOfMemoryError(当堆无法分配新对象时抛出)。


2.2.2栈(线程私有)

核心作用:存储方法调用的栈帧(Stack Frame),描述Java方法执行过程。
栈帧结构

局部变量表(Local Variable Table)

  • 存储方法参数、局部变量(基本类型+引用)
  • Slot(32位)为最小单位(long/double占2 Slot)

操作数栈(Operand Stack)

  • 存储计算过程的临时数据(如算术运算的操作数)
  • 基于栈的指令集核心(如iadd指令从栈顶弹出两个int相加后压回)

动态链接(Dynamic Linking)

  • 指向运行时常量池的方法引用(支持多态)

方法返回地址(Return Address)

  • 方法退出时恢复上层方法执行点
  • 异常StackOverflowError(栈深度超过-Xss限制)。

2.2.3本地方法栈(线程私有)

核心作用:支持本地方法(Native Method) 执行(如C/C++代码)。
特性

  • 与虚拟机栈功能类似,但服务于native方法(通过JNI调用)

  • HotSpot等实现中常与虚拟机栈合并
    异常StackOverflowError(本地方法调用链过深)。


2.2.4程序计数器(线程私有)

核心作用:记录当前线程正在执行的字节码指令地址
关键特性

  • 唯一无OutOfMemoryError的区域(占用极小固定内存)。

  • 执行Java方法时:记录字节码指令地址

  • 执行Native方法时:值为undefined(因执行引擎切换至本地代码)。

  • 线程切换依赖:恢复线程执行时,依赖PC寄存器定位执行点。


2.2.5方法区(线程共享)

核心作用:存储类元数据运行时常量池静态变量等。
演进与实现

JDK版本实现方式关键变化
JDK ≤7永久代(PermGen)受限于JVM堆内存(-XX:MaxPermSize
JDK ≥8元空间(Metaspace)使用本地内存-XX:MaxMetaspaceSize

存储内容: 

  1. 类信息(类名、父类、接口、字段描述符、方法字节码)

  2. 运行时常量池(符号引用 → 直接引用解析结果)

  3. 静态变量(static成员)

  4. JIT编译后的代码缓存(HotSpot的CodeCache)
    异常OutOfMemoryError(元空间耗尽时)。

2.2.6内存布局中的异常问题(堆溢出和栈溢出)

1. 堆溢出(OutOfMemoryError: Java heap space)

触发条件

  • 对象数量超过堆容量(如大数组、内存泄漏)
    示例代码

    List<byte[]> list = new ArrayList<>();
    while (true) {list.add(new byte[1024 * 1024]); // 持续分配1MB数组
    }

解决方案

  • 增大堆:-Xmx4g(设置最大堆为4GB)

  • 分析内存泄漏:MAT、JProfiler等工具定位GC Roots引用链

2. 栈溢出(StackOverflowError)

触发条件

  • 方法调用链过深(如无限递归)

  • 线程栈空间不足(-Xss设置过小)
    示例代码

    void recursiveCall() {recursiveCall();  // 无限递归
    }

解决方案

  • 增大栈容量:-Xss2m(设置线程栈为2MB)

  • 优化代码:避免深层递归或循环依赖


总结: 

区域线程共享性存储内容异常类型
共享对象实例、数组OutOfMemoryError
虚拟机栈私有栈帧(局部变量、操作数栈)StackOverflowError
本地方法栈私有Native方法调用信息StackOverflowError
程序计数器私有当前指令地址
方法区共享类元数据、常量池、静态变量OutOfMemoryError (JDK8+)

2.3JVM类加载

类加载是JVM将字节码文件(.class)加载到内存,并转换为运行时数据结构的过程。该过程分为加载(Loading)链接(Linking) 和初始化(Initialization) 三个阶段,其中链接又包含三个子阶段:

2.3.1类加载过程

2.3.1.1加载

核心任务:查找并加载类的二进制数据

  • 加载源:本地文件系统、网络、ZIP/JAR包、运行时计算生成(动态代理)等

  • 关键操作

    1. 通过类的全限定名获取其二进制字节流

    2. 将字节流代表的静态存储结构转化为方法区的运行时数据结构

    3. 在堆中生成java.lang.Class对象,作为方法区数据的访问入口

  • 类加载器角色ClassLoader.defineClass()实现字节流到Class对象的转换


2.3.1.2链接

(1) 验证(Verification)

目的:确保字节码安全且符合JVM规范

  • 文件格式验证:检查魔数(0xCAFEBABE)、版本号、常量池合法性

  • 元数据验证:语义分析(是否有父类、是否实现接口等)

  • 字节码验证:数据流和控制流分析(类型转换、跳转指令合法性)

  • 符号引用验证:检查引用的类/字段/方法是否存在且可访问

(2) 准备(Preparation)

核心任务:为类变量分配内存并设置初始值

  • 分配内存:在方法区为静态变量分配内存

  • 初始值设置:赋予类型默认值(如int=0boolean=false引用=null

  • 特殊处理static final常量直接赋程序设定值(编译期优化)

(3) 解析(Resolution)

核心任务:将符号引用转换为直接引用

  • 符号引用:用一组符号描述引用的目标(全限定名)

  • 直接引用:指向目标的指针、偏移量或句柄

  • 解析类型

    • 类/接口解析

    • 字段解析

    • 方法解析

    • 接口方法解析


2.3.1.3初始化

核心任务:执行类构造器<clinit>()方法

<clinit>方法生成

  • 编译器自动收集类中所有静态变量赋值静态代码块
  • 按源码顺序合并生成

执行规则

  • JVM保证子类<clinit>执行前父类<clinit>已完成
  • 多线程环境下会被正确加锁同步(线程安全)

触发时机(首次主动使用时):

  1. 创建类实例(new
  2. 访问静态变量/方法(非常量)
  3. 反射调用(Class.forName()
  4. 初始化子类(父类需先初始化)
  5. 包含main()方法的启动类
[加载] → 获取字节流 → 创建Class对象 → 方法区数据结构↓
[链接] → [验证] → 文件格式 → 元数据 → 字节码 → 符号引用↓[准备] → 静态变量分配内存 → 赋默认值↓[解析] → 符号引用 → 直接引用↓
[初始化] → 执行<clinit>() → 静态赋值/代码块

2.3.2双亲委派模型

2.3.2.1模型结构

JVM采用树状层级结构的类加载机制:

          启动类加载器(Bootstrap)↑扩展类加载器(Extension)↑应用程序类加载器(Application)↑自定义类加载器(Custom ClassLoader)

2.3.2.2核心加载器

(1) 启动类加载器(Bootstrap ClassLoader)

  • 实现:C++编写(HotSpot),JVM内部组件

  • 职责:加载核心库(JAVA_HOME/lib目录)

  • 特性

    • 唯一无父加载器的加载器

    • 加载java.*等核心包(如java.lang.String

(2) 扩展类加载器(Extension ClassLoader)

  • 实现sun.misc.Launcher$ExtClassLoader(Java)

  • 职责:加载扩展库(JAVA_HOME/lib/ext目录)

  • 特性

    • 父加载器是Bootstrap(但Java中表示为null

    • 加载javax.*等扩展包

(3) 应用程序类加载器(Application ClassLoader)

  • 实现sun.misc.Launcher$AppClassLoader

  • 职责:加载ClassPath用户类

  • 特性

    • 默认的上下文类加载器

    • ClassLoader.getSystemClassLoader()返回此加载器


2.3.2.3工作流程

当类加载请求发生时:

  1. 子加载器首先委托父加载器尝试加载

  2. 父加载器递归向上委托直至Bootstrap

  3. 若父加载器能完成加载,返回结果

  4. 若所有父加载器无法加载,子加载器才尝试加载

代码实现ClassLoader.loadClass()简化逻辑):

protected Class<?> loadClass(String name, boolean resolve) {synchronized (getClassLoadingLock(name)) {// 1. 检查是否已加载Class<?> c = findLoadedClass(name);if (c == null) {try {// 2. 委托父加载器if (parent != null) {c = parent.loadClass(name, false);} else {// 3. 到达Bootstrapc = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// 父加载器无法完成加载}// 4. 父加载器未找到,尝试自身加载if (c == null) {c = findClass(name);}}return c;}
}

2.3.2.4破坏双亲委派的场景

(1) SPI机制(Service Provider Interface)

  • 问题:JDBC等接口由Bootstrap加载,但实现类需由应用加载器加载

  • 解决方案:线程上下文类加载器(Thread Context ClassLoader)

// 获取上下文类加载器
ClassLoader loader = Thread.currentThread().getContextClassLoader();
// 加载服务实现
ServiceLoader<Driver> services = ServiceLoader.load(Driver.class, loader);

(2) OSGi模块化

  • 机制:每个模块(Bundle)使用独立类加载器

  • 特点

    • 平级类加载器间直接委托

    • 支持模块热部署

(3) 热部署实现

  • 技术原理:创建新的类加载器加载修改后的类

  • 实现方式:重写findClass()方法,不委托父加载器

类加载器关系图

[用户代码] → [自定义类加载器] → 委派 → [应用程序类加载器]↓委派 → [扩展类加载器]↓委派 → [启动类加载器]
2.3.2.5总结

类加载三阶段

  • 加载:二进制数据 → Class对象
  • 链接:验证(安全)→ 准备(内存分配)→ 解析(符号→直接引用)
  • 初始化:执行<clinit>()(静态初始化)

双亲委派核心价值

  • 安全屏障:防止核心API被篡改

    • 示例:自定义java.lang.String类会被Bootstrap优先加载,用户类无效

  • 避免重复加载:父加载器已加载的类,子加载器不会再次加载

  • 职责分离:不同加载器负责不同层次的类加载

破坏委派的合理场景

  • SPI服务加载(JDBC/JNDI)
  • 模块化热部署(OSGi/Tomcat)
  • 动态代码生成(Groovy/Scala REPL)

2.4垃圾回收GC

2.4.1死亡对象的判断算法

2.4.1.1引用计数法

核心原理:为每个对象维护引用计数器,记录被引用次数

Object A = new Object();  // A计数=1
Object B = A;             // A计数=2
A = null;                 // A计数=1
B = null;                 // A计数=0 → 可回收

优点

  • 实现简单

  • 实时性高,对象不再被引用时立即回收

致命缺陷

  • 循环引用问题:对象相互引用导致计数永不为0

    class Node {Node next;
    }Node a = new Node();  // a.count=1
    Node b = new Node();  // b.count=1
    a.next = b;          // b.count=2
    b.next = a;          // a.count=2
    a = b = null;        // a.count=1, b.count=1 → 内存泄漏!

结论:JVM不采用此算法


2.4.1.2可达性分析法

核心原理:从GC Roots对象出发,遍历引用链,不可达对象判定为死亡

GC Roots对象包括

  • 虚拟机栈中引用的对象

  • 方法区静态属性引用的对象

  • 方法区常量引用的对象

  • 本地方法栈JNI引用的对象

  • 同步锁持有的对象

  • JVM内部引用(如系统类加载器)

两次标记过程

  1. 第一次标记:筛选finalize()方法未被覆盖或已被调用过的对象

  2. 第二次标记:将对象放入F-Queue,由Finalizer线程执行finalize()

  3. finalize()中对象重新建立引用链→自救成功

回收流程

[GC Roots] → 枚举根节点 → 可达性分析 → 第一次标记 → 
执行finalize() → 第二次标记 → 回收内存

2.4.2垃圾回收算法

2.4.2.1标记-清除法

执行步骤

  • 标记:遍历GC Roots标记可达对象

  • 清除:回收未标记对象内存

    标记前: [A][B][C][D]  (A、C可达)
    清除后: [A][ ][C][ ]   (B、D被回收)

缺点

  • 内存碎片化(不连续空间)

  • 分配大对象时可能触发Full GC


2.4.2.2复制法

执行步骤

  1. 将内存分为Eden、Survivor0、Survivor1区

  2. 对象首先分配在Eden

  3. Minor GC时存活对象复制到Survivor1

  4. 清空Eden和Survivor0

  5. 交换Survivor0和Survivor1角色

GC前:
Eden: [A][B][C]  
S0: [D][E]  
S1: [空]GC后:
Eden: [空]
S0: [空]
S1: [A][D]  // B、C、E被回收

优点:无碎片、高效
缺点:内存利用率仅50%
应用:新生代回收


2.4.2.3标记-整理法

执行步骤

  1. 标记:同标记-清除

  2. 整理:存活对象向一端移动

  3. 清理边界外内存

GC前: [A][ ][B][C][ ]  (A、B、C存活)
GC后: [A][B][C][ ][ ]  (无碎片)

优点:无内存碎片
缺点:移动对象成本高
应用:老年代回收


2.4.2.4分代算法

核心思想:按对象生命周期划分内存区域

内存分配策略

  1. 新生代(占堆1/3)

    • Eden区(80%):新对象诞生地

    • Survivor区(20%):存活对象过渡区(S0+S1各占10%)

  2. 老年代(占堆2/3)

    • 长期存活对象(年龄>15)

    • 大对象直接进入(-XX:PretenureSizeThreshold)

对象晋升流程


GC类型对比

GC类型触发条件速度停顿时间算法
Minor GCEden区满复制算法
Major GC老年代满标记-清除/整理
Full GC老年代/方法区空间不足极慢很长混合算法

2.4.3垃圾收集器

这里只做简单的介绍,并不过多展开:

1. 新生代收集器

收集器并行方式算法特点适用场景启动参数
Serial单线程复制算法STW时间长,简单高效客户端模式、嵌入式系统-XX:+UseSerialGC
ParNew多线程复制算法Serial的多线程版需与CMS配合的老年代回收-XX:+UseParNewGC
Parallel Scavenge多线程复制算法吞吐量优先后台计算型应用-XX:+UseParallelGC

2. 老年代收集器

收集器并行方式算法特点缺点启动参数
Serial Old单线程标记-整理Serial的老年代版STW时间长-XX:+UseSerialGC
Parallel Old多线程标记-整理Parallel Scavenge的老年代版无并发能力-XX:+UseParallelOldGC
CMS并发标记-清除低延迟优先内存碎片、并发失败-XX:+UseConcMarkSweepGC

3. 全堆收集器(JDK 9+主流)

收集器并行方式算法突破性特点适用场景启动参数
G1并发+并行分区复制+标记整理可预测停顿模型
(200ms内)
大内存、低延迟要求-XX:+UseG1GC
ZGC全并发染色指针+读屏障亚毫秒级停顿
(<10ms)
超低延迟场景-XX:+UseZGC
Shenandoah全并发转发指针+读屏障与ZGC竞争的低延迟收集器类似ZGC

3.小结

最后让我们总结文章核心内容:

内存管理:

  • 承载对象生命周期,是GC主战场;维系方法调用链,深度决定递归极限
  • 元空间(Metaspace) 取代永久代,通过本地内存管理类元数据,显著降低OOM风险
  • 程序计数器作为线程执行锚点,唯一免疫内存溢出的区域

核心机制:

  • 双亲委派模型:通过层级加载保障类安全,SPI等场景需破坏此机制
  • 分代收集理论:基于对象年龄(新生代/老年代)匹配标记-复制/标记-整理算法
  • GC Roots可达性:以活动线程栈、静态变量等为根,判定对象存亡的绝对准则

JVM将物理内存抽象为逻辑数据区,将对象回收转化为算法问题。理解其内存布局与执行机制,是诊断性能瓶颈、规避内存泄漏的基石。正如Java语言架构师Brian Goetz所言:“虚拟机是Java生态的隐形操作系统”。掌握JVM,方能真正驾驭Java。

http://www.xdnf.cn/news/16423.html

相关文章:

  • OGG同步Oracle到Kafka不停库,全量加增量
  • 《汇编语言:基于X86处理器》第9章 编程练习
  • 新房装修是中央空调还是壁挂空调好?
  • 背包DP之完全背包
  • Agentic RAG理解和简易实现
  • UG创建的实体橘黄色实体怎么改颜色?
  • HCIP上HCIA复习静态综合实验
  • 【Java、C、C++、Python】飞机订票系统---文件版本
  • 基于springboot的小区车位租售管理系统
  • dart使用
  • 从入门到进阶:JavaScript 学习之路与实战技巧
  • C++学习笔记(十:类与对象基础)
  • 内存优化:从堆分配到零拷贝的终极重构
  • 【笔记】Handy Multi-Agent Tutorial 第四章: CAMEL框架下的RAG应用 (简介)
  • linux-开机启动流程
  • 蓝桥杯java算法例题
  • NOIP 模拟赛 7
  • ZYNQ芯片,SPI驱动开发自学全解析个人笔记【FPGA】【赛灵思
  • 同声传译新突破!字节跳动发布 Seed LiveInterpret 2.0
  • Win11批量部署神器winget
  • 滚珠导轨:手术机器人与影像设备的精密支撑
  • 升级目标API级别到35,以Android15为目标平台(三 View绑定篇)
  • 上位机程序开发基础介绍
  • Round-Robin仲裁器
  • 深入理解 BIO、NIO、AIO
  • RocketMQ学习系列之——客户端消息确认机制
  • jwt 在net9.0中做身份认证
  • [2025CVPR-图象分类方向]CATANet:用于轻量级图像超分辨率的高效内容感知标记聚合
  • C# WPF 实现读取文件夹中的PDF并显示其页数
  • 案例分享|告别传统PDA+便携打印机模式,快速实现高效率贴标