JVM内存模型
目录
1、什么是JVM?
2、为什么要学习JVM?
(1)深入理解Java程序的运行机制
(2)优化程序性能
(3)排查线上问题
(4)编写高效代码
(5)跨平台与语言无关性
(6)面试与职业发展
3、JVM的体系结构
3.1 类加载子系统(Class Loader SubSystem)
(1) 加载(Loading)
(2) 链接(Linking)
(3) 初始化(Initialization)
3.2 运行时数据区(Runtime Data Areas)
(1) 线程私有区域(每个线程独立)
(2) 线程共享区域(所有线程共用--->非线程安全的)
3.3 执行引擎(Execution Engine)
(1) 解释器(Interpreter)
(2) JIT编译器(Just-In-Time Compiler)
(3) 垃圾回收(Garbage Collection)
(4) 本地方法接口(JNI, Java Native Interface)
3.4 数据流向总结
4、在Java 8版本之后的更新
4.1 类加载子系统的改进
4.2 运行时数据区的重大变化
(1) 方法区 → 元空间(Metaspace)
(2) 堆内存与垃圾回收器
(3) 线程栈优化
4.3 执行引擎的增强
4.4 工具链与监控的演进
4.5 总结
5、新特性探索(Java 8+)
模块一:Java 8~Java 11
5.1 模块化(Java 9+)
5.2 元空间(Metaspace)取代永久代
5.3 GC 行为变化(CMS 移除)
模块二:Java 11~Java 17
5.4 低延迟 GC:ZGC 与 Shenandoah
5.5 GraalVM 原生镜像(AOT 编译)
5.6 虚拟线程(Loom 项目,Java 19 正式)
模块三:新特性详解
5.7 元空间调优
5.8 G1/ZGC 原理
5.9 JFR(Java Flight Recorder)诊断
1、什么是JVM?
- Java Virtual Machine(Java虚拟机),用来保证Java语言跨平台。
- Java虚拟机可以看做是一台抽象的计算机,如同真实的计算机那样,它有自己的指令集以及各种运行时内存区域。
JVM模拟了真实计算机的架构,拥有独立的指令集(如字节码指令)和内存管理机制(如堆、栈、方法区等)。这种设计使得Java程序无需直接操作物理硬件,而是通过JVM的统一接口运行,从而实现跨平台性。例如,javac
编译生成的字节码(.class文件)是JVM的“指令集”,而堆内存用于存储对象实例,栈内存用于方法调用。
- Java虚拟机与Java语言并没有必然的联系,它只与特定的二进制文件格式(class文件格式)所关联。
JVM的核心规范是处理符合class文件格式
的二进制文件,而非特定语言。这意味着任何能编译成合规字节码的语言(如Kotlin、Scala)均可运行在JVM上。例如,Groovy编写的代码编译后也能被JVM执行,体现了JVM的“语言无关性”。
- Java虚拟机就是一个字节码翻译器,它将字节码文件翻译成各个系统对应的机器码,确保字节码文件能在各个系统正确运行。
JVM通过解释或即时编译(JIT)将字节码转换为目标系统的原生机器码。例如,在Windows和Linux上,同一份.class文件会被JVM分别翻译为x86或ARM指令,确保程序在不同OS上行为一致。这种“一次编译,到处运行”的特性依赖JVM对各平台的适配。
2、为什么要学习JVM?
学习 JVM(Java虚拟机) 是Java开发者进阶的必经之路,主要原因包括以下几个方面:
(1)深入理解Java程序的运行机制
- Java程序不是直接运行在操作系统上,而是通过JVM执行字节码。
- 了解JVM如何加载类、管理内存、执行字节码等,能帮助你写出更高效、稳定的代码。
- 例如:理解
ClassLoader
机制可以避免类加载冲突,优化启动性能。
(2)优化程序性能
- JVM的内存管理(堆、栈、方法区)、垃圾回收(GC)机制直接影响程序性能。
- 掌握JVM调优(如调整堆大小、选择合适的GC算法)可以显著提升高并发、大数据量场景下的应用性能。
- 例如:
-Xms
和-Xmx
参数调整堆内存,避免频繁GC导致的卡顿。
(3)排查线上问题
- 线上环境可能出现
OOM(内存溢出)
、CPU飙升
、线程死锁
等问题,掌握JVM工具(如jstack
、jmap
、VisualVM
)看懂GC日志就能够快速定位问题。 - 例如:用
jstack
分析线程堆栈,找出死锁或阻塞的代码。
(4)编写高效代码
- 了解JVM内存模型(如
逃逸分析
、栈上分配
)可以优化对象创建方式,减少GC压力。 - 例如:
String
的intern()
方法可以复用字符串常量池,减少内存占用。
(5)跨平台与语言无关性
- JVM不仅支持Java,还支持Kotlin、Scala、Groovy等JVM语言,理解JVM有助于学习这些语言的底层机制。
- 例如:Kotlin的协程底层依赖JVM的线程模型。
(6)面试与职业发展
- JVM是Java面试的核心考点之一(如GC、类加载、内存模型),掌握JVM能提升竞争力。
- 高级开发、架构师等岗位通常要求深入理解JVM调优和底层原理。
3、JVM的体系结构
如下图所示,这是一个JVM的体系结构图(基于Java8的环境):
这张JVM体系结构图清晰地展示了Java虚拟机的主要组成部分及其运行流程,主要包括 类加载子系统(Class Loader SubSystem)、运行时数据区(Runtime Data Areas) 和 执行引擎(Execution Engine) 三大部分。下面我们逐一解析每个模块的功能和流程。
3.1 类加载子系统(Class Loader SubSystem)
负责运行时首次引用类文件时加载.class
文件到内存,并转换为JVM可识别的数据结构。类加载过程分为 加载(Loading)→ 链接(Linking)→ 初始化(Initialization) 三个阶段:
(1) 加载(Loading)
- 任务:查找并加载
.class
文件(二进制字节码)到内存。 - 类加载器(Class Loader):
- Bootstrap Class Loader(启动类加载器):加载
JRE/lib
下的核心类库(如java.lang.*
)。 - Extension Class Loader(扩展类加载器):加载
JRE/lib/ext
下的扩展类。 - Application Class Loader(应用程序类加载器):加载用户程序的类(
classpath
下的类)。
- Bootstrap Class Loader(启动类加载器):加载
(2) 链接(Linking)
- 验证(Verify):检查字节码是否符合JVM规范(如魔数
0xCAFEBABE
),如果验证失败将会收到验证错误。 - 准备(Prepare):为 静态变量 分配内存并设置默认值(如
int
初始化为0
)。 - 解析(Resolve):将符号引用(如类名、方法名)转换为直接引用(内存地址)。
(3) 初始化(Initialization)
- 执行
<clinit>()
方法(静态变量赋值、执行静态代码块)。 - 例如:
static int a = 100;
在此阶段赋值。
3.2 运行时数据区(Runtime Data Areas)
JVM运行时的内存模型,分为 线程私有 和 线程共享 区域:
(1) 线程私有区域(每个线程独立)
- 程序计数器(PC Register)
- 记录当前线程执行的字节码指令地址(线程切换后恢复执行位置)。
- PS:每一个线程都有自己的程序计数器。
- 虚拟机栈(Stack Area)
- 存储 栈帧(Stack Frame),每个方法调用对应生成一个栈帧,所有的局部变量都会在栈内存创建,其中栈帧包含:
- 局部变量表(LVA):存放方法参数和局部变量。
- 操作数栈(Operand Stack):用于计算中间结果(如
i++
操作)。 - 动态链接(Dynamic Linking):指向运行时常量池的方法引用。
- 方法返回地址:方法执行完毕后返回的位置。
- 存储 栈帧(Stack Frame),每个方法调用对应生成一个栈帧,所有的局部变量都会在栈内存创建,其中栈帧包含:
- 本地方法栈(Native Method Stack)
- 为
native
方法(如C/C++代码)提供运行环境。 - PS:对应的,每个线程都会创建自己的本地方法栈
- 为
(2) 线程共享区域(所有线程共用--->非线程安全的)
- 堆(Heap Area)
- 存储 所有对象实例和数组,是垃圾回收(GC)的主要区域。
- 分为 新生代(Young Gen) 和 老年代(Old Gen)。
- 方法区(Method Area)
- 存储 类信息、常量、静态变量、JIT编译后的代码(JDK 8后由 元空间Metaspace 实现)。
- PS:每个Java虚拟机都只有一个方法区,所以他是公共的。
3.3 执行引擎(Execution Engine)
负责执行字节码,核心组件包括:
(1) 解释器(Interpreter)
- 逐行解释执行字节码(启动快,但执行效率低)。
- 当一个方法被调用多次的时候,他还是会逐一解释,所以JIT编译器应运而生。
(2) JIT编译器(Just-In-Time Compiler)
- 热点代码优化:将频繁执行的字节码(比如上文提到的多次调用一个方法的重复代码)编译为本地机器码(本地代码会直接用于系统调用,从而提升性能)。
- 工作流程:
- Profiler(分析器):识别热点代码(如循环、高频调用方法)。
- Code Optimizer(代码优化器):优化字节码(如内联、逃逸分析)。
- Target Code Generator(目标代码生成器):生成机器码。
(3) 垃圾回收(Garbage Collection)
- 自动回收堆内存中的无用对象(如引用计数、可达性分析算法)。
-
我们可以通过调用
System.gc()方法触发垃圾回收器,但是不能保证执行。
(4) 本地方法接口(JNI, Java Native Interface)
- 调用本地方法(如
System.currentTimeMillis()
底层用C实现)。
3.4 数据流向总结
- 类加载:
.class
文件 → 类加载子系统 → 方法区(存储类信息)。 - 运行阶段:
- 线程栈执行方法 → 操作数栈计算 → 堆内存创建对象。
- JIT优化热点代码 → 生成机器码加速执行。
- 垃圾回收:堆内存中的无用对象被GC清理。
4、在Java 8版本之后的更新
4.1 类加载子系统的改进
模块 | Java 8 | Java 11/17 |
---|---|---|
类加载器 | Bootstrap/Extension/Application | 模块化系统(Java 9+):引入 Layer 分层加载机制,支持动态模块依赖。 |
加载过程 | 固定路径(JRE/lib, ext, classpath) | 模块路径(module-path) 替代 classpath,增强隔离性。 |
元数据存储 | 永久代(PermGen)存储类元数据 | 元空间(Metaspace):类元数据移至本地内存,默认无上限(-XX:MaxMetaspaceSize 可限制)。 |
影响:
- 永久代 OOM 问题消失,但需监控元空间占用(如滥用动态类生成仍会触发 Metaspace OOM)。
- 模块化减少类冲突(如
javax
vsjakarta
),但需适应新打包方式(jlink
生成定制化运行时)。
4.2 运行时数据区的重大变化
(1) 方法区 → 元空间(Metaspace)
- Java 8: Method Area (PermGen)- 固定大小(`-XX:PermSize=64m`),易触发 `OutOfMemoryError: PermGen space`。- 存储:类元数据、运行时常量池、静态变量。+ Java 17: Metaspace- 本地内存管理,默认自动扩展(受限于物理内存)。- 存储:仅类元数据(静态变量移至堆中)。- 调优:`-XX:MaxMetaspaceSize=256m` 限制上限。
(2) 堆内存与垃圾回收器
特性 | Java 8 | Java 11/17 |
---|---|---|
默认 GC | Parallel GC(吞吐优先) | G1 GC(平衡吞吐/延迟) |
低延迟 GC | 无 | ZGC(亚毫秒级停顿,Java 11+) Shenandoah(低停顿,Java 12+) |
堆分区 | 固定新生代/老年代比例 | G1 的 Region-Based 动态分区 |
影响:
- ZGC/Shenandoah 适用于微服务、实时系统(如支付交易),但需显式启用:
java -XX:+UseZGC -jar app.jar
- G1 的默认化减少调优负担,但需理解 Region 和 Mixed GC 机制。
(3) 线程栈优化
- Java 15+ 虚拟线程(预览):
- 轻量级线程(JEP 425),减少栈内存占用和上下文切换开销。
- 替代传统
Thread
模型,适用于高并发(如 10W+ 连接)。
Thread.startVirtualThread(() -> {...}); // Java 19 正式发布
4.3 执行引擎的增强
组件 | Java 8 | Java 11/17 |
---|---|---|
JIT 编译器 | C1/C2 分层编译 | Graal JIT(Java 10+ 可选) |
AOT 编译 | 不支持 | GraalVM AOT(生成原生镜像,Java 17+) |
本地方法 | 传统 JNI | Project Panama(预览,简化 FFI 调用) |
影响:
- Graal JIT:更激进的优化(如逃逸分析),但可能增加编译时间。
- AOT 编译:提升启动速度(如 Spring Native),但牺牲动态性(反射需配置)。
native-image -jar app.jar # 生成原生可执行文件
4.4 工具链与监控的演进
工具 | Java 8 | Java 11/17 |
---|---|---|
监控工具 | jstat、jmap、VisualVM | JDK Mission Control (JMC) Java Flight Recorder (JFR) |
命令行 | jcmd 功能有限 | 增强版 jcmd (支持 Metaspace/ZGC 诊断):jcmd <pid> VM.metaspace |
日志 | GC 日志需手动解析 | 统一日志(-Xlog):-Xlog:gc*=debug:file=gc.log |
示例(Java 17 ZGC 日志分析):
java -XX:+UseZGC -Xlog:gc*=info:file=zgc.log -jar app.jar
4.5 总结
Java 11/17 的 JVM 在 内存模型(元空间)、垃圾回收(ZGC)、执行效率(Graal/AOT)和 可观测性(JFR)上大幅改进,但核心架构(类加载、堆栈模型)仍与 Java 8 一脉相承。
5、新特性探索(Java 8+)
模块一:Java 8~Java 11
5.1 模块化(Java 9+)
- 问题背景:
图中Class Loader SubSystem
的Bootstrap/Extension/Application
三层加载器在 Java 8 中依赖classpath
,易引发 JAR 地狱(如版本冲突)。 - Java 11 解决方案:
- 模块化系统(JPMS):
- 类加载改为 模块路径(module-path) 替代
classpath
,每个模块声明依赖(module-info.java
)。 - 新增
Layer
分层加载机制,支持动态模块化部署。
- 类加载改为 模块路径(module-path) 替代
- 影响:
- 需重构大型项目为模块(如拆分
com.example
为独立模块)。 - 工具链变化:
jlink
生成定制化运行时,剔除未用模块减小体积。jlink --add-modules java.base --output minimal-jre
- 需重构大型项目为模块(如拆分
- 模块化系统(JPMS):
5.2 元空间(Metaspace)取代永久代
- 与上图对比:
原图的Method Area
(永久代)在 Java 8 中通过-XX:PermSize
固定大小,易触发OutOfMemoryError: PermGen space
。 - Java 11 变化:
- 元空间(Metaspace)特性:
- 类元数据移至 本地内存,默认无上限(受限于物理内存)。
- 字符串常量池、静态变量移至 堆内存。
- 调优参数:
-XX:MaxMetaspaceSize=256m # 限制上限 -XX:MetaspaceSize=64m # 初始大小
- 监控命令:
jcmd <pid> VM.metaspace # 查看元空间使用详情
- 元空间(Metaspace)特性:
5.3 GC 行为变化(CMS 移除)
- 图中对比:
原图Execution Engine
的Garbage Collection
未明确收集器类型,Java 8 默认Parallel GC
,CMS 为老年代低停顿选项。 - Java 11 更新:
- CMS 移除:改用 G1(Garbage-First) 作为默认收集器。
- G1 核心改进:
- Region-Based 堆分区:取代固定新生代/老年代比例,动态调整 Region 用途。
- Mixed GC 模式:同时回收新生代和老年代 Region。
- 调优示例:
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 # 目标停顿时间
模块二:Java 11~Java 17
5.4 低延迟 GC:ZGC 与 Shenandoah
- ZGC(Java 11+):
- 目标:亚毫秒级停顿(<1ms),适合实时交易系统。
- 原理:
- 染色指针(Colored Pointers):在指针中存储元数据,加速标记。
- 并发压缩:无需停顿线程整理内存。
- 启用命令:
-XX:+UseZGC -Xmx16g -Xlog:gc*=info:file=zgc.log
- Shenandoah(Java 12+):
- 类似 ZGC,但通过 Brooks 指针实现并发压缩,适用于大堆(>32GB)。
5.5 GraalVM 原生镜像(AOT 编译)
- 图中对比:
原图Execution Engine
的JIT Compiler
(C1/C2)在 Java 17 中可替换为 Graal JIT 或 AOT 编译。 - 原生镜像特性:
- 将 Java 程序编译为独立可执行文件(无需 JVM),启动速度提升 10x。
- 限制:反射、动态类加载需提前配置(
reflect-config.json
)。 - 示例:
native-image -jar app.jar --no-fallback
5.6 虚拟线程(Loom 项目,Java 19 正式)
- 背景:
原图的Stack Area
中每个线程固定占用 ~1MB 栈内存,高并发时成本太高了。 - 虚拟线程:
- 轻量级线程(协程),由 JVM 调度,复用 OS 线程。
- 支持百万级并发(传统线程仅千级)。
- 示例:
Thread.startVirtualThread(() -> System.out.println("Virtual Thread"));
模块三:新特性详解
5.7 元空间调优
- 监控工具:
- JMC/JFR:实时监控 Metaspace 使用率与 GC 事件。
- Arthas:诊断类加载泄漏(如动态生成类未卸载)。
- 常见问题:
- Metaspace OOM:因滥用 CGLIB/ASM 动态生成类,需限制大小或优化代码。
5.8 G1/ZGC 原理
收集器 | G1(Java 8+) | ZGC(Java 11+) |
---|---|---|
目标 | 平衡吞吐与停顿(~200ms) | 亚毫秒级停顿(<1ms) |
堆布局 | 固定大小 Region | 动态 Region(无分代) |
标记 | 并发标记 + SATB 算法 | 并发染色指针 |
压缩 | 并行压缩(Mixed GC 阶段停顿) | 并发压缩(无停顿) |
5.9 JFR(Java Flight Recorder)诊断
- 启用命令:
-XX:StartFlightRecording=delay=5s,duration=60s,name=myrecording,filename=recording.jfr
- 关键事件:
jdk.GCPhase
:分析 GC 停顿时间。jdk.MetaspaceAllocationFailure
:定位元空间问题。
- 工具:
- JMC 可视化分析
.jfr
文件。
- JMC 可视化分析
通过对比原图与新版特性,可以清晰看到 JVM 从 固定内存模型 向 动态化、低延迟、云原生 的演进方向。