JVM原理总结

JVM原理解析:内存模型、GC机制、类加载、执行引擎与调优实战

Java 虚拟机(JVM)是 Java 语言跨平台、自动内存管理、高性能 的核心支撑。本文将从 JVM 整体架构、内存模型、类加载机制、执行引擎、垃圾回收(GC)、内存分配、调优工具与参数 七个维度,全面拆解 JVM 的底层原理,覆盖从字节码执行到内存优化的全链路知识,帮你构建完整的 JVM 知识体系。

一、JVM 整体架构:程序运行的"骨架"

JVM 的核心目标是加载并执行 Java 字节码文件,其架构由三大核心模块和辅助模块组成,各模块协同工作完成程序的编译与运行。

复制代码
JVM 整体架构 = 类加载子系统 + 运行时数据区 + 执行引擎 + 辅助工具(垃圾回收器、本地方法接口)
  • 类加载子系统 :负责将 .class 文件加载到内存,并完成验证、准备、解析、初始化,最终生成可被 JVM 使用的类对象。
  • 运行时数据区:JVM 运行时的内存空间,分为线程私有和线程共享区域,是内存管理的核心。
  • 执行引擎:负责执行字节码指令,包括解释器、即时编译器(JIT)、垃圾回收器等核心组件。
  • 本地方法接口(JNI):连接 Java 代码与 C/C++ 等本地代码,实现跨语言调用。
  • 本地方法库:存放本地方法的具体实现,如操作系统底层 API 封装。

二、运行时数据区:JVM 的"内存布局"

运行时数据区是 JVM 管理内存的核心区域,根据《Java 虚拟机规范(Java SE 8)》,其分为 线程私有区域线程共享区域,不同区域有明确的功能和生命周期。

2.1 线程私有区域(与线程同生共死)

线程私有区域的生命周期与所属线程一致,线程创建时分配内存,线程销毁时释放内存,不存在多线程共享冲突问题。

内存区域 核心作用 内部结构 异常类型
程序计数器 1. 记录当前线程执行的字节码指令地址(行号) 2. 支持线程切换后恢复执行(线程上下文切换的核心) 3. 执行 Native 方法时,计数器值为 undefined 无复杂结构,仅存储指令地址 唯一不会抛出 OOM 的区域
虚拟机栈 存储方法调用的 栈帧(Stack Frame),每个方法从调用到执行完毕对应一个栈帧的入栈和出栈 栈帧包含: 1. 局部变量表:存储方法参数和局部变量 2. 操作数栈:执行字节码指令的临时数据栈 3. 动态链接:指向运行时常量池的方法引用 4. 方法出口:记录方法执行完毕后返回的地址 1. StackOverflowError:栈深度超过虚拟机允许的最大值(如无限递归) 2. OutOfMemoryError:栈容量动态扩展时无法申请到足够内存
本地方法栈 为 Native 方法(如 java.lang.Thread.start0())提供内存空间 结构与虚拟机栈类似,具体实现依赖底层操作系统 同虚拟机栈

2.2 线程共享区域(随 JVM 启动/关闭而创建/销毁)

线程共享区域被所有线程共享,是内存泄漏、OOM 异常的高发区,也是 GC 的核心操作区域。

内存区域 核心作用 版本差异 异常类型
堆(Heap) 存储 对象实例数组,是 JVM 最大的内存区域 所有版本一致,可通过 -Xms(初始堆大小)、-Xmx(最大堆大小)参数调整 OutOfMemoryError: Java heap space:堆空间不足,无法创建新对象
方法区(Method Area) 存储 类信息 (类名、父类、接口、字段、方法)、运行时常量池静态变量即时编译器编译后的代码缓存 1. Java 7 及以前:通过 永久代(PermGen) 实现,受 JVM 内存限制 2. Java 8 及以后:移除永久代,改用 元空间(Metaspace),直接使用本地内存,默认仅受物理内存限制 1. Java 7:OutOfMemoryError: PermGen space 2. Java 8+:OutOfMemoryError: Metaspace
运行时常量池 方法区的一部分,存储编译期生成的 字面量 (如字符串常量)和 符号引用 (如类名、方法名),运行时可动态添加常量(如 String.intern() 1. Java 7:从永久代移至堆 2. Java 8+:属于元空间 同方法区
堆的细分:为 GC 优化而生

为了提高垃圾回收效率,堆被进一步划分为 新生代老年代 ,比例默认是 1:2(可通过 -XX:NewRatio 调整)。

  • 新生代 :存储新创建的对象,特点是对象存活率低、回收频繁 ,采用标记-复制算法
    • 细分区域:Eden 区(占 80%)、From Survivor 区(占 10%)、To Survivor 区(占 10%),比例可通过 -XX:SurvivorRatio 调整。
  • 老年代 :存储长期存活的对象,特点是对象存活率高、回收频率低 ,采用标记-清除算法标记-整理算法
  • 永久代/元空间 :注意!永久代/元空间 不属于堆,是方法区的实现,很多人容易混淆。

三、类加载机制:字节码的"加载与初始化"

类加载子系统负责将磁盘上的 .class 文件加载到 JVM 内存,并转换为 java.lang.Class 对象,整个过程分为 5 个阶段 ,其中加载、验证、准备、初始化 四个阶段的顺序是固定的,解析阶段 可在初始化之后执行(支持动态绑定)。

3.1 类加载的 5 个核心阶段

阶段 核心操作 关键细节
加载(Loading) 1. 通过类的全限定名获取 .class 文件的二进制字节流 2. 将字节流转换为方法区的运行时数据结构 3. 在堆中生成一个代表该类的 Class 对象,作为方法区数据的访问入口 加载的来源:本地文件、网络(如 RMI)、动态生成(如 动态代理)、数据库等
验证(Verification) 确保 .class 文件的字节流符合 JVM 规范,防止恶意字节码攻击 验证内容:文件格式验证(如魔数 0xCAFEBABE)、元数据验证、字节码验证、符号引用验证
准备(Preparation) 为类的静态变量 分配内存,并设置默认初始值 (如 int 默认为 0,boolean 默认为 false 注意:不会执行赋值语句 ,如 public static int a = 1;,准备阶段 a 的值是 0,初始化阶段才会赋值为 1
解析(Resolution) 将常量池中的 符号引用 转换为 直接引用(如将类名转换为内存地址) 解析对象:类或接口、字段、方法、方法类型等
初始化(Initialization) 执行类的 静态代码块静态变量赋值语句,是类加载过程中唯一由程序员控制的阶段 初始化触发条件: 1. 首次访问类的静态变量或静态方法 2. 创建类的实例(new 关键字) 3. 反射调用类的方法(Class.forName()) 4. 初始化子类时,父类会先初始化 5. JVM 启动时,执行主类(main 方法所在类)

3.2 类加载器与双亲委派模型

类加载器是实现"加载"阶段的核心组件,JVM 提供了 3 种内置类加载器,同时支持自定义类加载器。

3.2.1 内置类加载器
类加载器 加载范围 父加载器
启动类加载器(Bootstrap ClassLoader) 加载 JRE/lib 目录下的核心类库(如 rt.jar),由 C++ 实现,不属于 Java 类
扩展类加载器(Extension ClassLoader) 加载 JRE/lib/ext 目录下的扩展类库,由 Java 实现 启动类加载器
应用程序类加载器(Application ClassLoader) 加载用户类路径(ClassPath)下的类,由 Java 实现 扩展类加载器
3.2.2 双亲委派模型

双亲委派模型 是类加载器的核心工作机制,其核心规则是:

  1. 当一个类加载器收到加载请求时,首先委托父加载器加载,而非自己直接加载。
  2. 父加载器无法加载该类时(在自己的加载范围内找不到),子加载器才会尝试自己加载。

优点

  • 避免类的重复加载:确保同一个类在 JVM 中只有一个 Class 对象。
  • 保证核心类库的安全:防止用户自定义的类(如 java.lang.String)替换 JVM 核心类。

破坏场景

  • 为了实现热部署(如 Tomcat 的 WebappClassLoader)。
  • 为了实现动态代理(如 JDK 动态代理)。

四、执行引擎:字节码的"翻译与执行"

执行引擎是 JVM 的"心脏",负责将运行时数据区中的字节码指令转换为机器指令执行,核心组件包括 解释器即时编译器(JIT)垃圾回收器

4.1 解释器

  • 原理 :采用逐行解释执行的方式,将字节码指令翻译为机器指令,执行一条,翻译一条。
  • 优点:启动速度快,适合短时间运行的程序(如脚本)。
  • 缺点:执行效率低,因为每次执行都需要重新翻译。
  • 核心组件Bytecode Interpreter,JVM 启动时默认使用解释器执行。

4.2 即时编译器(JIT)

为了解决解释器执行效率低的问题,JVM 引入了即时编译器,其核心目标是将热点代码编译为机器码,提高执行效率

4.2.1 热点代码的判定

JVM 通过 热点计数器 判定热点代码,分为两种计数器:

  • 方法调用计数器:统计方法被调用的次数,超过阈值(默认 10000)则标记为热点方法。
  • 回边计数器:统计循环体执行的次数,超过阈值则标记为热点循环。
4.2.2 JIT 编译的优化策略

JIT 编译器会对热点代码进行一系列优化,常见优化手段包括:

  1. 方法内联:将被调用的小方法的代码直接嵌入调用方,减少方法调用开销。
  2. 逃逸分析 :分析对象的作用域,若对象未逃逸出方法,则可进行栈上分配 (避免 GC)、标量替换 (将对象拆分为基本类型)、同步消除(移除无用的锁)。
  3. 常量折叠 :将编译期可知的常量表达式直接计算结果,如 int a = 1 + 2; 优化为 int a = 3;
4.2.3 JIT 的两种编译器

HotSpot 虚拟机提供了两种 JIT 编译器,可通过参数调整:

  • C1 编译器:轻量级编译器,编译速度快,优化程度较低,适合客户端程序。
  • C2 编译器:重量级编译器,编译速度慢,优化程度高,适合服务端程序。
  • 分层编译(Java 7+ 默认开启):结合 C1 和 C2 的优点,先由 C1 编译,再由 C2 进一步优化。

4.3 本地方法接口(JNI)

当字节码执行到 native 方法时,执行引擎会通过 JNI 调用本地方法库中的 C/C++ 实现,流程如下:

  1. JVM 加载本地方法库(如 System.loadLibrary())。
  2. 将 Java 类型转换为 C/C++ 类型(如 jint 对应 int)。
  3. 调用本地方法的实现函数。
  4. 将 C/C++ 执行结果转换为 Java 类型并返回。

五、垃圾回收(GC)机制:内存的"自动清洁工"

GC 是 JVM 最核心的特性之一,其目标是 自动识别并回收不再被引用的对象 ,释放内存空间,避免内存泄漏和 OOM 异常。GC 的核心流程是:判断对象存活 → 选择回收算法 → 执行回收操作

5.1 判断对象是否存活的核心算法

这是 GC 的前提,只有确定对象"无用",才能进行回收。

算法 原理 优点 缺点 JVM 应用
引用计数法 给每个对象添加一个引用计数器,被引用时 +1,引用失效时 -1;计数器为 0 的对象可回收 实现简单,判定效率高 无法解决循环引用问题(如 A 引用 B,B 引用 A,两者无外部引用,但计数器不为 0) 未采用
可达性分析算法 GC Roots 为起点,向下遍历对象引用链;若对象不在任何引用链上,则判定为可回收对象 解决了循环引用问题 实现复杂,需要暂停用户线程(STW) HotSpot 虚拟机默认采用
GC Roots 的组成

GC Roots 是 JVM 中公认的"存活对象",包括以下 4 类:

  1. 虚拟机栈中局部变量表引用的对象。
  2. 本地方法栈中 Native 方法引用的对象。
  3. 方法区中类静态属性引用的对象。
  4. 方法区中常量引用的对象。
引用类型的扩展

Java 提供了 4 种引用类型,不同类型的对象有不同的回收策略:

引用类型 回收时机 应用场景
强引用 只有当强引用失效时,对象才会被回收 普通对象引用(如 Object obj = new Object()
软引用 内存不足时,对象会被回收 缓存(如 SoftReference
弱引用 每次 GC 时,对象都会被回收 缓存(如 WeakReferenceWeakHashMap
虚引用 随时可能被回收,仅用于跟踪对象的回收状态 管理直接内存(如 PhantomReferenceCleaner

5.2 核心垃圾回收算法

不同算法适用于不同的内存区域,各有优劣,JVM 采用 分代收集算法 结合多种基础算法。

算法 核心步骤 优点 缺点 适用区域
标记-清除算法(Mark-Sweep) 1. 标记:遍历所有对象,标记可回收对象 2. 清除:遍历堆,回收标记对象的内存 实现简单,无需移动对象 1. 产生内存碎片 (大量不连续的内存块,无法分配大对象) 2. 效率低(标记和清除均需遍历全堆) 老年代
标记-复制算法(Mark-Copy) 1. 将内存分为两块大小相等的区域,只用其中一块 2. 标记存活对象,复制到另一块区域 3. 清空原区域的所有对象 1. 无内存碎片 2. 回收效率高 内存利用率低(仅 50%) 新生代(优化:只划分一块 Eden 区和两块小的 Survivor 区,利用率提升至 90%)
标记-整理算法(Mark-Compact) 1. 标记可回收对象 2. 将存活对象向内存一端移动,紧凑排列 3. 清除边界外的所有对象 1. 无内存碎片 2. 内存利用率高 效率低(需移动对象,涉及内存拷贝) 老年代
分代收集算法 结合新生代和老年代的特点: 1. 新生代:对象存活率低,用标记-复制算法 2. 老年代:对象存活率高,用标记-清除/标记-整理算法 兼顾效率和内存利用率 实现复杂 整个堆(JVM 默认算法)

5.3 常用垃圾收集器

垃圾收集器是回收算法的具体实现,不同收集器适用于不同的应用场景,HotSpot 虚拟机提供了多种收集器,可通过参数指定。

收集器 适用区域 核心特点 垃圾回收停顿(STW) 应用场景 JVM 参数
Serial GC 新生代 + 老年代 单线程回收,采用标记-复制(新生代)+ 标记-整理(老年代) STW 时间长 单核 CPU、小型应用(如桌面程序) -XX:+UseSerialGC
ParNew GC 新生代 Serial GC 的多线程版本,支持与 CMS 配合 STW 时间比 Serial 短 多核 CPU、追求低延迟的应用 -XX:+UseParNewGC
Parallel Scavenge GC 新生代 多线程回收,目标是提高吞吐量(运行用户代码时间/总时间) STW 时间较短 后台运算、批量处理任务(如数据统计) -XX:+UseParallelGC
Parallel Old GC 老年代 Parallel Scavenge 的老年代版本,采用标记-整理算法 STW 时间较短 与 Parallel Scavenge 配合,适用于高吞吐量场景 -XX:+UseParallelOldGC
CMS GC(Concurrent Mark Sweep) 老年代 基于标记-清除算法,分 4 步执行: 1. 初始标记(STW,标记 GC Roots 直接引用的对象) 2. 并发标记(无 STW,遍历引用链) 3. 重新标记(STW,修正并发标记的偏差) 4. 并发清除(无 STW,回收对象) STW 时间极短 追求低延迟的 Web 应用(如电商、金融) -XX:+UseConcMarkSweepGC
G1 GC(Garbage-First) 整个堆 将堆划分为多个大小相等的 Region,兼顾吞吐量和低延迟,支持预测性 STW 时间 STW 时间可预测 大型应用、多核 CPU 环境(如服务器) -XX:+UseG1GC
ZGC 整个堆 利用着色指针和读屏障技术,几乎无停顿,支持 TB 级内存 停顿时间 < 10ms 超大型应用、低延迟要求极高的场景(如云计算) -XX:+UseZGC
Shenandoah GC 整个堆 与 ZGC 类似,低停顿,支持大内存 停顿时间 < 10ms 超大型应用 -XX:+UseShenandoahGC

六、内存分配与回收策略:对象的"安家落户"

JVM 对对象的内存分配遵循**"优先新生代,晋升老年代"的原则,同时针对大对象、长期存活对象有特殊策略,核心目标是减少 GC 次数,提高程序性能**。

6.1 核心分配策略

  1. 对象优先在 Eden 区分配

    • 当创建新对象时,JVM 优先将对象分配到新生代的 Eden 区。
    • 当 Eden 区空间不足时,触发 Minor GC (新生代 GC):将 Eden 和 From Survivor 中存活的对象复制到 To Survivor 区,然后清空 Eden 和 From Survivor 区;同时将对象的年龄计数器 +1
  2. 大对象直接进入老年代

    • 大对象指需要大量连续内存空间的对象(如长字符串、大数组)。
    • 为了避免大对象在新生代中频繁复制(浪费时间),JVM 提供参数 -XX:PretenureSizeThreshold,超过该阈值的对象直接分配到老年代。
  3. 长期存活的对象进入老年代

    • 每个对象都有一个年龄计数器,每经历一次 Minor GC 且存活,年龄 +1。
    • 当对象年龄达到阈值(默认 15),会被晋升到老年代,阈值可通过 -XX:MaxTenuringThreshold 调整。
  4. 动态年龄判断

    • 新生代的 Survivor 区中,相同年龄的所有对象大小总和超过 Survivor 区的一半时,年龄大于等于该年龄的对象,可直接晋升老年代,无需等待阈值。
  5. 老年代空间分配担保

    • 在触发 Minor GC 前,JVM 会检查老年代最大可用连续空间是否大于新生代所有对象总大小。
    • 若大于,则 Minor GC 安全执行;若小于,则检查 -XX:HandlePromotionFailure 参数是否开启:
      • 开启:尝试执行 Minor GC,失败则触发 Full GC(整堆回收)。
      • 关闭:直接触发 Full GC。

6.2 特殊分配策略:栈上分配

通过 JIT 的逃逸分析 ,若对象未逃逸出方法(即对象的作用域仅限于方法内部),JVM 会将对象分配在虚拟机栈的局部变量表中,而非堆中。

  • 优点:对象随方法执行完毕而销毁,无需 GC 参与,减少内存开销。
  • 开启参数-XX:+DoEscapeAnalysis(Java 7+ 默认开启)。

七、JVM 调优

JVM 调优的核心目标是 减少 GC 次数、降低 STW 时间、提高程序吞吐量或降低延迟 ,调优的前提是明确业务场景(吞吐量优先或延迟优先)。

7.1 调优的核心步骤

  1. 监控运行状态:使用工具收集 JVM 运行数据,如堆内存使用情况、GC 次数、STW 时间。
  2. 分析瓶颈:根据监控数据定位问题,如频繁 Full GC、STW 时间过长、内存泄漏等。
  3. 调整参数:根据瓶颈调整 JVM 参数,如堆大小、收集器类型、新生代比例等。
  4. 验证效果:重新运行程序,监控调优后的指标,迭代优化。

7.2 常用调优工具

工具 核心功能 适用场景
jps 列出正在运行的 JVM 进程 查看进程 ID
jstat 监控 JVM 内存和 GC 状态 实时查看堆内存使用、GC 次数、STW 时间
jmap 生成堆转储快照(heap dump 分析内存泄漏、大对象分布
jhat 分析堆转储快照 查看对象数量、引用关系
jstack 生成线程快照(thread dump 排查死锁、线程阻塞
VisualVM 可视化监控工具,整合 jps、jstat、jmap 等功能 图形化分析 JVM 运行状态
GCViewer 分析 GC 日志 可视化 GC 趋势、STW 时间分布

7.3 核心调优参数

参数分类 核心参数 作用 示例
堆内存参数 -Xms 设置初始堆大小,建议与 -Xmx 相同,避免堆动态扩展 -Xms2g
-Xmx 设置最大堆大小 -Xmx4g
-XX:NewRatio 设置新生代与老年代的比例,默认 2(新生代:老年代=1:2) -XX:NewRatio=1
-XX:SurvivorRatio 设置 Eden 区与 Survivor 区的比例,默认 8(Eden:From:To=8:1:1) -XX:SurvivorRatio=6
收集器参数 -XX:+UseSerialGC 使用 Serial 收集器 -
-XX:+UseParallelGC 使用 Parallel Scavenge 收集器 -
-XX:+UseConcMarkSweepGC 使用 CMS 收集器 -
-XX:+UseG1GC 使用 G1 收集器 -
GC 日志参数 -XX:+PrintGCDetails 打印详细 GC 日志 -
-XX:+PrintGCTimeStamps 打印 GC 发生的时间戳 -
-Xloggc:/path/to/gc.log 将 GC 日志输出到指定文件 -
其他参数 -XX:MaxTenuringThreshold 设置对象晋升老年代的年龄阈值,默认 15 -XX:MaxTenuringThreshold=10
-XX:PretenureSizeThreshold 设置大对象直接进入老年代的阈值 -XX:PretenureSizeThreshold=1048576(1MB)
-XX:+DoEscapeAnalysis 开启逃逸分析 -

八、核心总结

JVM 的运行是一个多模块协同工作的复杂过程,核心知识点可归纳为:

  1. 架构:类加载子系统加载字节码,运行时数据区管理内存,执行引擎执行指令。
  2. 内存:线程私有区域(程序计数器、虚拟机栈、本地方法栈)和线程共享区域(堆、方法区),堆是 GC 的核心。
  3. 类加载:5 个阶段 + 双亲委派模型,保证类的安全与唯一性。
  4. 执行引擎:解释器 + JIT 编译器,兼顾启动速度和执行效率;逃逸分析实现栈上分配,优化性能。
  5. GC:可达性分析判断对象存活,分代收集算法是核心策略,不同收集器适配不同业务场景。
  6. 调优:监控 → 分析 → 调整 → 验证,核心是根据业务场景选择合适的收集器和参数。
相关推荐
fei_sun2 小时前
【总结】【OS】成组链接法
jvm·数据结构
7ioik4 小时前
JVM 核心参数调优清单
jvm
CodeAmaz6 小时前
JVM一次完整GC流程详解
java·jvm·gc流程
笃行客从不躺平8 小时前
JVM 类加载机制复习
jvm
飞火流星020278 小时前
【Arthas工具】使用Trace命令分析Java JVM方法调用链路及耗时
java·jvm·arthas·jvm性能调优·java方法调用链路分析及耗时·jvm实时分析·jvm方法调用实时分析
7ioik8 小时前
JVM 调优工具深度指南:从监控到诊断的全流程实战
jvm
喵手9 小时前
JVM 基础知识:深入理解 Java 的运行时环境!
java·jvm·jvm基础·java运行环境
WizLC1 天前
【JAVA】JVM类加载器知识笔记
java·jvm·笔记
CodeAmaz1 天前
Java 垃圾回收(GC)算法详解
java·jvm·算法·垃圾回收算法