# JVM 深入解析
# 一、JVM 核心概念与概述
# 1. JVM 简介
Java 虚拟机(JVM, Java Virtual Machine)是 Java 平台的核心组成部分,它负责将 Java 字节码转换为特定平台的机器码并执行。JVM 提供了跨平台能力,使得 Java 程序可以在不同的操作系统上运行,实现了"一次编写,到处运行"(Write Once, Run Anywhere)的特性。
JVM 的主要功能包括:
- 字节码解释和执行
- 内存管理(分配和回收)
- 线程管理
- 安全机制
- 异常处理
- 类加载
# 2. JVM 的架构模型
JVM 采用基于栈的架构模型,与基于寄存器的架构相比,基于栈的架构具有更好的跨平台性,但在执行效率上可能略逊一筹。
基于栈的架构特点:
- 指令集小,编译器容易实现
- 无需硬件支持,可移植性强
- 执行过程依赖栈操作,指令多
基于寄存器的架构特点:
- 指令集大,执行效率高
- 依赖硬件实现,可移植性差
- 指令少,执行速度快
# 3. JVM 版本演变
JVM 随着 Java 版本的更新而不断演进,每个大版本都会引入性能优化和新特性。主要版本包括:
- JDK 1.0-1.4:早期版本,性能和功能相对有限
- JDK 5.0:引入了 JVM 性能提升、泛型等重要特性
- JDK 6:提升了 JVM 性能,引入了 JMX 等监控工具
- JDK 7:G1 垃圾收集器(实验性)、invokedynamic 指令
- JDK 8:元空间替代永久代、G1 成为默认收集器
- JDK 9:模块化系统(Jigsaw)、ZGC 垃圾收集器(实验性)
- JDK 11:ZGC 正式发布、Epsilon 收集器
- JDK 17:增强 ZGC、移除实验性的 AOT 编译器
- JDK 21:分代 ZGC
# 二、JVM 架构详解
# 1. 类加载子系统
类加载子系统负责查找和加载类文件,并将其转换为 JVM 可识别的内部表示。类加载过程分为以下几个阶段:
加载(Loading):
- 通过类名查找类文件
- 将类文件内容加载到内存
- 创建 Class 对象表示这个类
链接(Linking):
- 验证(Verification): 确保类文件格式正确、符合 JVM 规范
- 准备(Preparation): 为静态变量分配内存并设置默认值
- 解析(Resolution): 将符号引用替换为直接引用
初始化(Initialization):
- 执行静态初始化块
- 为静态变量赋予正确的初始值
使用(Using):
- 类实例化和方法调用
卸载(Unloading):
- 类不再使用时被 GC 回收
# 2. 运行时数据区
运行时数据区是 JVM 内存管理的核心区域,分为以下几个部分:
程序计数器(Program Counter Register):
- 记录当前执行的字节码位置
- 线程私有,每个线程都有独立的程序计数器
- 如果执行的是 native 方法,计数器值为 undefined
Java 虚拟机栈(Java Virtual Machine Stack):
- 线程私有,存储方法执行的栈帧
- 每个方法执行时会创建一个栈帧,包含局部变量表、操作数栈、动态链接、方法出口等
- 可能抛出 StackOverflowError(栈溢出)和 OutOfMemoryError(内存不足)
本地方法栈(Native Method Stack):
- 与虚拟机栈类似,但用于执行 native 方法
- 线程私有
- 可能抛出 StackOverflowError 和 OutOfMemoryError
Java 堆(Java Heap):
- 所有线程共享,存储对象实例和数组
- 垃圾收集器主要工作区域
- 可分为年轻代(Young Generation)和老年代(Old Generation)
- 可能抛出 OutOfMemoryError
方法区(Method Area):
- 所有线程共享,存储类结构、常量、静态变量、即时编译器编译后的代码等
- JDK 8 之前使用永久代(Permanent Generation)实现
- JDK 8 及以后使用元空间(Metaspace)实现,元空间使用本地内存
- 可能抛出 OutOfMemoryError
运行时常量池(Runtime Constant Pool):
- 方法区的一部分,存储编译期生成的各种字面量和符号引用
- 具有动态性,运行时也可以添加常量(如 String.intern())
# 3. 执行引擎
执行引擎负责将字节码转换为特定平台的机器码并执行,主要包括以下组件:
解释器(Interpreter):
- 逐行解释字节码并执行
- 启动速度快,但执行效率低
即时编译器(Just-In-Time Compiler, JIT):
- 将热点代码(频繁执行的代码)编译为本地机器码
- 执行效率高,但编译需要时间
- 分为 C1 编译器(客户端编译器)和 C2 编译器(服务器编译器)
- JDK 10 引入了 Graal 编译器作为实验性 JIT 编译器
垃圾收集器(Garbage Collector):
- 自动回收不再使用的对象内存
- 无需程序员手动管理内存
本地方法接口(Native Interface):
- 与本地方法库交互的接口
- 允许 Java 调用本地方法(如 C/C++ 实现的方法)
# 三、JVM 内存管理
# 1. 内存分配策略
JVM 根据对象的不同特性采用不同的内存分配策略:
对象优先在 Eden 区分配:
- 大多数对象在 Eden 区分配内存
- 当 Eden 区空间不足时,触发 Minor GC
大对象直接进入老年代:
- 大对象(如大数组)直接在老年代分配
- 避免大对象在 Eden 区和 Survivor 区之间频繁复制
长期存活的对象进入老年代:
- 对象每经历一次 Minor GC 仍存活,年龄增加 1
- 当年龄达到阈值(默认 15)时,进入老年代
动态对象年龄判定:
- 如果在 Survivor 区中相同年龄的所有对象大小总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代
- 无需等待年龄达到阈值
# 2. 内存回收策略
JVM 采用分代收集的内存回收策略,根据对象的存活周期将内存划分为不同区域:
Minor GC:
- 发生在年轻代的垃圾回收
- 速度快,频率高
- 采用复制算法
Major GC/Full GC:
- Major GC 发生在老年代的垃圾回收
- Full GC 是对整个堆进行垃圾回收
- 速度慢,频率低
- 通常采用标记-清除或标记-整理算法
Stop-The-World:
- 垃圾收集器执行时,所有应用线程都会暂停
- 不同的收集器有不同的暂停时间
# 3. 内存泄漏与溢出
内存泄漏(Memory Leak):
- 程序中已不再使用的对象无法被 GC 回收
- 常见原因:长生命周期对象持有短生命周期对象的引用、静态集合类使用不当、数据库连接未关闭等
内存溢出(OutOfMemoryError):
- 程序请求的内存超过了 JVM 所能提供的最大内存
- 常见类型:
- Java heap space:堆内存不足
- PermGen space/Metaspace:方法区内存不足
- StackOverflowError:栈溢出
- Requested array size exceeds VM limit:请求的数组大小超过了 JVM 限制
# 四、垃圾收集器
# 1. 垃圾收集算法
标记-清除算法(Mark-Sweep):
- 先标记所有需要回收的对象,然后统一回收
- 优点:不需要移动对象
- 缺点:产生内存碎片,影响程序运行效率
复制算法(Copying):
- 将内存分为两块,每次只使用其中一块
- 垃圾回收时,将存活对象复制到另一块,然后清除使用过的内存
- 优点:不会产生内存碎片
- 缺点:内存利用率低
标记-整理算法(Mark-Compact):
- 先标记所有需要回收的对象,然后将存活对象向一端移动,最后清除边界以外的内存
- 优点:不会产生内存碎片,内存利用率高
- 缺点:需要移动对象,效率较低
分代收集算法(Generational Collection):
- 根据对象存活周期的不同将内存分为不同区域
- 年轻代:对象存活率低,采用复制算法
- 老年代:对象存活率高,采用标记-清除或标记-整理算法
# 2. 常见垃圾收集器
Serial 收集器:
- 单线程收集器
- 简单高效,适用于单 CPU 环境
- 收集时会 Stop-The-World
- 年轻代使用复制算法,老年代使用标记-整理算法
Parallel 收集器:
- Serial 的多线程版本
- 关注吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))
- 适用于后台计算等对延迟要求不高的场景
CMS(Concurrent Mark Sweep)收集器:
- 以获取最短回收停顿时间为目标
- 基于标记-清除算法
- 分为初始标记、并发标记、重新标记、并发清除四个阶段
- 优点:并发收集、低延迟
- 缺点:对 CPU 资源敏感、产生内存碎片、无法处理浮动垃圾
G1(Garbage First)收集器:
- JDK 9 及以后的默认收集器
- 基于标记-整理算法
- 将堆分为多个大小相等的独立区域(Region)
- 可以精确控制停顿时间
- 优先回收价值最大的区域
- 适用于大堆内存
ZGC(Z Garbage Collector):
- JDK 11 引入的低延迟收集器
- 支持 TB 级别的堆内存
- 停顿时间控制在毫秒级别
- 并发执行所有阶段
- 基于标记-整理算法
- 使用着色指针和读屏障技术
Shenandoah 收集器:
- 与 ZGC 类似,也是低延迟收集器
- 主要区别在于使用连接指针而非着色指针实现并发压缩
Epsilon 收集器:
- JDK 11 引入的无操作收集器
- 不进行任何垃圾回收操作
- 适用于性能测试、内存压力测试等场景
# 3. 收集器选择策略
选择合适的垃圾收集器需要考虑以下因素:
- 应用类型: 桌面应用、Web 应用、批处理应用等
- 硬件配置: CPU 核心数、内存大小
- 性能目标: 吞吐量优先还是延迟优先
- 内存占用: 应用程序的内存需求
推荐的选择策略:
- 单 CPU 环境:Serial 收集器
- 多 CPU 环境,吞吐量优先:Parallel 收集器
- 多 CPU 环境,延迟优先:CMS、G1、ZGC 或 Shenandoah 收集器
- 大堆内存(> 8GB):G1、ZGC 或 Shenandoah 收集器
# 五、JVM 类加载机制
# 1. 类加载器分类
JVM 自带的类加载器包括:
Bootstrap Class Loader(启动类加载器):
- 最顶层的类加载器
- 负责加载 Java 核心类库(如 rt.jar)
- 使用 C/C++ 实现
- 没有父加载器
- 加载的类位于 <JAVA_HOME>/lib 目录
Extension Class Loader(扩展类加载器):
- 负责加载 Java 扩展类库
- 使用 Java 实现
- 父加载器是 Bootstrap Class Loader
- 加载的类位于 <JAVA_HOME>/lib/ext 目录
Application Class Loader(应用程序类加载器):
- 负责加载应用程序的类路径上的类
- 使用 Java 实现
- 父加载器是 Extension Class Loader
- 是默认的类加载器
自定义类加载器:
- 用户可以通过继承 java.lang.ClassLoader 类实现自定义类加载器
- 用于满足特殊需求,如加密解密、热部署等
# 2. 类加载机制的特点
双亲委派模型(Parents Delegation Model):
- 类加载器收到类加载请求时,首先委托给父加载器加载
- 父加载器无法加载时,才由子加载器尝试加载
- 优点:避免类的重复加载、保护 Java 核心类库的安全
打破双亲委派模型的情况:
- 线程上下文类加载器(Thread Context ClassLoader):用于加载 SPI 实现类
- OSGi 模块化框架:支持热部署和动态加载
- Tomcat 等容器:为每个 Web 应用提供独立的类加载环境
# 3. 类的生命周期
类的生命周期包括以下阶段:
- 加载(Loading): 查找并加载类文件
- 链接(Linking): 验证、准备、解析
- 初始化(Initialization): 执行静态初始化块和静态变量赋值
- 使用(Using): 实例化对象、调用方法
- 卸载(Unloading): 类不再使用时被 GC 回收
类初始化的触发条件:
- 创建类的实例
- 调用类的静态方法
- 访问类的静态字段(final 常量除外)
- 使用反射方式强制创建类或接口的 Class 对象
- 初始化一个类的子类
- 启动类(包含 main 方法的类)
# 六、JVM 调优
# 1. JVM 调优基础
调优目标:
- 提高应用程序的响应速度
- 提高应用程序的吞吐量
- 减少垃圾收集的暂停时间
- 减少内存占用
调优前的准备:
- 确定性能瓶颈
- 建立性能基准线
- 设置合理的调优目标
# 2. 常用 JVM 参数
堆内存相关参数:
-Xms<size>
:初始堆大小,如-Xms2g
-Xmx<size>
:最大堆大小,如-Xmx4g
-Xmn<size>
:年轻代大小,如-Xmn2g
-XX:SurvivorRatio=<ratio>
:Eden 区与 Survivor 区的比例,如-XX:SurvivorRatio=8
(表示 Eden:Survivor=8:1)-XX:MaxTenuringThreshold=<age>
:对象进入老年代的年龄阈值,如-XX:MaxTenuringThreshold=15
垃圾收集器相关参数:
-XX:+UseSerialGC
:使用 Serial 收集器-XX:+UseParallelGC
:使用 Parallel 收集器-XX:+UseConcMarkSweepGC
:使用 CMS 收集器-XX:+UseG1GC
:使用 G1 收集器-XX:+UseZGC
:使用 ZGC 收集器
GC 日志相关参数:
-XX:+PrintGCDetails
:打印详细的 GC 日志-XX:+PrintGCDateStamps
:打印 GC 发生的时间戳-Xloggc:<file>
:指定 GC 日志文件路径,如-Xloggc:gc.log
-XX:+HeapDumpOnOutOfMemoryError
:发生 OOM 时生成堆转储文件-XX:HeapDumpPath=<path>
:指定堆转储文件路径
JIT 相关参数:
-XX:CompileThreshold=<invocations>
:方法调用多少次后进行 JIT 编译-XX:+TieredCompilation
:启用分层编译
# 3. 调优策略
年轻代调优:
- 适当增加年轻代大小,减少 Minor GC 频率
- 调整 SurvivorRatio,避免对象过早进入老年代
- 对于短期对象多的应用,增大年轻代比例
老年代调优:
- 选择合适的垃圾收集器
- 调整老年代大小,避免频繁的 Full GC
- 监控老年代内存增长情况,找出内存泄漏
GC 调优:
- 减少 Stop-The-World 时间
- 调整 GC 触发时机
- 监控 GC 日志,分析 GC 行为
内存泄漏排查:
- 使用堆转储分析工具(如 MAT、VisualVM)分析内存泄漏
- 监控对象创建和销毁情况
- 检查长生命周期对象引用
# 七、JVM 监控与诊断工具
# 1. 命令行工具
jps(Java Process Status):
- 显示 Java 进程的 PID 和主类名
- 常用命令:
jps -l
(显示完整包名)、jps -v
(显示 JVM 参数)
jstat(JVM Statistics Monitoring Tool):
- 监控 JVM 的运行状态和统计信息
- 常用命令:
jstat -gc <pid>
(显示 GC 相关信息)、jstat -class <pid>
(显示类加载信息)
jinfo(Java Configuration Info):
- 查看和修改 JVM 的配置参数
- 常用命令:
jinfo <pid>
(显示所有参数)、jinfo -flag <flag> <pid>
(显示特定参数)
jmap(Java Memory Map):
- 生成堆转储文件和查看内存使用情况
- 常用命令:
jmap -heap <pid>
(显示堆内存信息)、jmap -dump:format=b,file=<file> <pid>
(生成堆转储文件)
jhat(Java Heap Analysis Tool):
- 分析堆转储文件
- 常用命令:
jhat <heap-dump-file>
jstack(Java Stack Trace):
- 生成线程堆栈信息,用于排查线程问题
- 常用命令:
jstack <pid>
(显示所有线程堆栈)
jcmd(Java Command):
- 多功能命令行工具,可以执行多种 JVM 相关操作
- 常用命令:
jcmd <pid> VM.flags
(显示 JVM 参数)、jcmd <pid> GC.run
(执行 GC)
# 2. 可视化工具
VisualVM:
- 集成了多种 JVM 监控和诊断功能
- 支持内存分析、线程分析、性能分析等
- 可以安装插件扩展功能
JConsole:
- JDK 自带的监控工具
- 提供内存、线程、类加载等监控功能
- 支持图表化显示
MAT(Memory Analyzer Tool):
- 专业的堆内存分析工具
- 用于分析内存泄漏和内存使用情况
- 支持多种报表和分析视图
JMC(Java Mission Control):
- 高级性能分析和诊断工具
- 集成了 JFR(Java Flight Recorder)功能
- 提供详细的 JVM 运行数据
JProfiler:
- 商业性能分析工具
- 支持 CPU、内存、线程等多维度分析
- 提供详细的热点分析和内存泄漏检测
# 八、JVM 实践案例
# 1. 常见性能问题排查
案例一:应用程序响应缓慢
问题现象: 应用程序响应时间变长,用户体验下降。
可能原因:
- GC 频繁或 GC 暂停时间过长
- 内存不足,频繁进行内存分配和回收
- 线程死锁或阻塞
- 数据库查询缓慢
排查步骤:
- 使用 jstat 监控 GC 情况,查看是否有频繁的 Full GC
- 使用 jstack 查看线程状态,检查是否有死锁或阻塞
- 使用 VisualVM 或 JProfiler 分析应用程序的性能热点
- 检查数据库连接池和查询性能
优化方案:
- 调整 JVM 堆大小和 GC 策略
- 优化内存使用,减少对象创建
- 修复线程死锁问题
- 优化数据库查询和索引
案例二:OutOfMemoryError 异常
问题现象: 应用程序抛出 OutOfMemoryError 异常并崩溃。
可能原因:
- 堆内存不足
- 内存泄漏
- 大对象分配
- 方法区(元空间)内存不足
排查步骤:
- 检查错误信息,确定 OOM 的类型(Java heap space、PermGen space/Metaspace 等)
- 分析 GC 日志,查看内存使用趋势
- 使用 jmap 生成堆转储文件
- 使用 MAT 等工具分析堆转储文件,找出内存占用大的对象或内存泄漏点
优化方案:
- 增加 JVM 堆大小或元空间大小
- 修复内存泄漏问题
- 优化大对象的创建和使用
- 调整 GC 策略
# 2. JVM 调优实践
案例:Web 应用程序调优
应用特点:
- 典型的 Web 应用,处理大量 HTTP 请求
- 有大量短期对象和部分长期对象
- 对响应时间有较高要求
调优前配置:
- JVM 参数:
-Xms2g -Xmx2g
- 默认 GC 收集器
调优步骤:
- 监控应用程序的内存使用和 GC 情况
- 发现 Full GC 频率较高,且暂停时间较长
- 调整 GC 收集器为 G1
- 调整年轻代和老年代的比例
- 监控调优后的效果
调优后配置:
- JVM 参数:
-Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:G1HeapRegionSize=16m
调优效果:
- Full GC 频率从每小时 2-3 次减少到每天 1-2 次
- GC 暂停时间从平均 500ms 减少到平均 80ms
- 应用程序响应时间明显改善
# 九、JVM 最新发展趋势
# 1. 性能优化
JVM 持续在性能优化方面进行改进,主要包括:
- 垃圾收集器优化: ZGC、Shenandoah 等低延迟收集器的成熟和普及
- JIT 编译器优化: Graal 编译器的发展和应用
- 内存管理优化: 减少内存占用,提高内存利用率
- 启动时间优化: 减少 JVM 启动时间,提升应用程序的启动速度
# 2. 新特性和改进
JDK 新版本中 JVM 相关的新特性和改进包括:
- GraalVM: 提供即时编译、提前编译和多语言支持的高性能运行时
- AOT 编译: 提前编译 Java 代码为本地机器码,提高启动性能
- JFR 增强: Java Flight Recorder 的功能增强,提供更详细的运行时数据
- JVM 工具链改进: 提供更强大、更易用的监控和诊断工具
# 3. 云原生支持
随着云原生技术的发展,JVM 也在不断增强对云环境的支持:
- 容器感知: JVM 能够自动感知容器环境的资源限制
- 内存使用优化: 减少 JVM 在容器环境中的内存占用
- 弹性伸缩支持: 优化 JVM 在弹性伸缩环境中的表现
- 轻量级运行时: 提供更轻量级的 JVM 运行时,适合微服务架构
通过不断的优化和创新,JVM 继续保持其在企业级应用开发中的核心地位,为 Java 应用程序提供高性能、可靠、安全的运行环境。
← ☕ Java 21 新特性详解 并发编程 →