JVM(Java Virtual Machine)是 Java 语言 "一次编写,到处运行" 的核心保障,它本质是一个跨平台的虚拟计算机,负责将 Java 字节码(.class 文件)解释或编译为本地机器指令执行,同时管理内存、垃圾回收、线程调度等核心功能,屏蔽了底层操作系统的差异。
本文将从 JVM 核心组成、内存模型、垃圾回收、类加载、调优实践等关键维度,系统梳理 JVM 的核心知识。
一、JVM 核心组成架构
JVM 的架构遵循 "规范定义 + 实现各异" 的原则(如 HotSpot、J9、Zing 等实现,其中 HotSpot 是 Oracle JDK 默认实现,应用最广泛)。核心组成分为 5 大模块:
| 模块 | 核心作用 |
|---|---|
| 类加载子系统 | 负责加载.class 文件到内存,完成 "加载 - 验证 - 准备 - 解析 - 初始化" 流程,生成 Class 对象。 |
| 运行时数据区(内存模型) | JVM 的内存核心区域,存储程序运行时的所有数据(如对象、线程栈、常量等),是调优重点。 |
| 执行引擎 | 将字节码转换为本地机器指令执行,包含解释器(逐行执行)、JIT 编译器(热点代码编译优化)、垃圾回收器(GC)。 |
| 本地方法接口(JNI) | 提供 Java 代码调用 C/C++ 等本地方法的能力(如System.currentTimeMillis()底层依赖 JNI)。 |
| 本地方法库 | 存放 JNI 调用的本地方法实现(操作系统相关的原生库,如.dll/.so 文件)。 |
核心流程:Java 源码(.java)→ 编译器(javac)→ 字节码(.class)→ 类加载子系统 → 运行时数据区 → 执行引擎(解释 / 编译)→ 本地机器指令。
二、JVM 运行时数据区(内存模型)
JVM 内存模型是面试和调优的核心,JDK 8 及以后的内存结构(删除永久代,引入元空间)如下(线程私有 / 共享是关键区分点):
1. 线程私有区域(每个线程独立创建,线程退出后销毁)
(1)程序计数器(Program Counter Register)
- 作用:记录当前线程执行的字节码指令地址(行号),线程切换后能恢复到正确执行位置。
- 特点:
- 内存占用极小,无 GC(垃圾回收);
- 唯一不会抛出
OutOfMemoryError(OOM)的区域。
- 场景:多线程切换时的 "上下文恢复" 依赖此区域。
(2)Java 虚拟机栈(Java Virtual Machine Stack)
-
- 作用:存储线程执行方法时的栈帧(每个方法调用对应一个栈帧),栈帧包含:局部变量表(方法内变量)、操作数栈(计算临时数据)、动态链接(指向常量池的引用)、方法返回地址。
- 特点:
- 先进后出(FILO),线程私有;
- 栈深度默认 1~1024KB(可通过
-Xss参数调整,如-Xss256k);
- 异常:
- 栈深度超出限制 →
StackOverflowError(如递归调用无终止条件); - 栈扩容时内存不足 →
OutOfMemoryError。
- 栈深度超出限制 →
(3)本地方法栈(Native Method Stack)
- 作用:与虚拟机栈类似,但专门为 JNI 调用的本地方法(C/C++ 方法)服务。
- 特点:线程私有,无统一规范(不同 JVM 实现差异大);
- 异常:同样可能抛出
StackOverflowError和OutOfMemoryError。
2. 线程共享区域(所有线程共用,JVM 启动时创建,关闭时销毁)
(1)Java 堆(Java Heap)
- 作用:存储所有对象实例和数组(是 JVM 内存中最大的区域),是垃圾回收(GC)的核心区域。
- 特点:
- 线程共享,物理上可分为多个线程私有的分配缓冲区(TLAB),提升对象分配效率;
- 逻辑上分为 "年轻代" 和 "老年代"(GC 分代回收的基础);
- 参数配置(核心调优参数):
-Xms:堆初始大小(如-Xms2g,建议与-Xmx一致,避免频繁扩容);-Xmx:堆最大大小(如-Xmx4g,限制堆的最大内存占用);-XX:NewSize/-XX:MaxNewSize:年轻代初始 / 最大大小(也可用-Xmn直接指定年轻代大小,如-Xmn1g);
- 异常:堆内存不足(对象无法分配)→
OutOfMemoryError: Java heap space。
(2)方法区(Method Area)
- 作用:存储已加载的类信息(类名、字段、方法、接口)、常量池(字符串常量、数字常量)、静态变量、即时编译器(JIT)编译后的代码等。
- 特点:
- 线程共享,逻辑上属于堆的一部分(又称 "非堆");
- JDK 7 及以前:用 "永久代(PermGen)" 实现,受
-XX:PermSize/-XX:MaxPermSize控制; - JDK 8 及以后:废除永久代,改用 "元空间(Metaspace)" 实现(元空间物理上占用本地内存,而非 JVM 堆内存);
- 元空间参数配置:
-XX:MetaspaceSize:元空间初始大小(默认约 21MB,触发 Full GC 的阈值);-XX:MaxMetaspaceSize:元空间最大大小(默认无限制,建议手动指定,避免占用过多本地内存);
- 异常:
- JDK 7:永久代溢出 →
OutOfMemoryError: PermGen space(如频繁动态生成类); - JDK 8+:元空间溢出 →
OutOfMemoryError: Metaspace。
- JDK 7:永久代溢出 →
(3)运行时常量池(Runtime Constant Pool)
- 作用:方法区的一部分,存储.class 文件中的 "常量池表"(编译期生成的字面量、符号引用),以及运行时动态生成的常量(如
String.intern()方法生成的字符串)。 - 异常:常量池满 →
OutOfMemoryError: PermGen space(JDK7)或元空间溢出(JDK8+)。
三、垃圾回收(GC)核心原理
GC 是 JVM 自动管理内存的核心机制,负责回收堆和方法区中 "不再被引用" 的对象,释放内存。核心问题:哪些对象需要回收?如何回收?何时回收?
1. 垃圾判定算法(哪些对象要回收?)
(1)引用计数法(淘汰)
- 原理:给每个对象分配一个引用计数器,被引用时 + 1,引用失效时 - 1,计数器为 0 则标记为垃圾。
- 缺陷:无法解决 "循环引用" 问题(如 A 引用 B,B 引用 A,两者均无其他引用,计数器仍为 1,无法回收)。
- 现状:HotSpot 等主流 JVM 不使用。
(2)可达性分析算法(主流)
- 原理:以 "GC Roots" 为起点,遍历对象引用链,不可达的对象标记为垃圾(可回收)。
- GC Roots(根对象)包括:
- 虚拟机栈中局部变量表的引用对象(如方法内的变量);
- 本地方法栈中 JNI 的引用对象;
- 方法区中静态变量和常量的引用对象;
- 活跃线程的引用对象。
- 补充:对象被标记为垃圾后,并非立即回收,需经历 "两次标记"(判断是否重写
finalize()方法),最终确认无引用才会被回收。
2. 引用类型(影响对象回收时机)
Java 中引用分为 4 种,强度从高到低:
| 引用类型 | 特点 | 场景示例 |
|---|---|---|
| 强引用(Strong) | 普通引用(如Object obj = new Object()),GC 绝不会回收被强引用的对象。 |
普通对象存储 |
| 软引用(Soft) | 内存不足时(即将 OOM),GC 会回收软引用对象。 | 缓存(如图片缓存) |
| 弱引用(Weak) | 每次 GC 都会回收弱引用对象(无论内存是否充足)。 | 临时数据存储(如WeakHashMap) |
| 虚引用(Phantom) | 最弱引用,无法通过虚引用获取对象,仅用于监听对象被 GC 回收的事件。 | 堆外内存回收通知 |
3. 垃圾回收算法(如何回收?)
(1)分代回收算法(核心思想)
- 依据:对象的 "生命周期特性"(大部分对象朝生夕死,少数对象长期存活),将堆分为 "年轻代" 和 "老年代",采用不同回收算法。
- 堆的分代结构(默认比例,可通过参数调整):
- 年轻代(Young Gen):占堆的 1/3 左右,分为 Eden 区(80%)、Survivor0(S0,10%)、Survivor1(S1,10%);
- 老年代(Old Gen):占堆的 2/3 左右,存储长期存活的对象。
(2)年轻代回收算法:复制算法(Copying)
- 原理:
- 新对象优先分配到 Eden 区,Eden 区满时触发 "Minor GC"(年轻代 GC);
- 存活的对象被复制到 S0 区,清空 Eden 和 S1 区;
- 下次 Minor GC 时,存活对象复制到 S1 区,清空 Eden 和 S0 区(S0 和 S1 区交替使用,始终有一个为空);
- 对象在 S0/S1 区之间复制次数达到阈值(默认 15,可通过
-XX:MaxTenuringThreshold调整),则晋升到老年代。
- 优点:效率高(只复制存活对象),无内存碎片;
- 缺点:需要额外的空闲空间(S1 区),空间利用率低。
(3)老年代回收算法:标记 - 清除(Mark-Sweep)+ 标记 - 整理(Mark-Compact)
-
标记 - 清除算法:
- 标记:遍历所有对象,标记存活对象;
- 清除:回收未标记的垃圾对象,释放内存。
-
优点:无需额外空间,空间利用率高;
-
缺点:
- 效率低(标记 + 清除两次遍历);
- 产生内存碎片(导致大对象无法分配,触发 Full GC)。
-
标记 - 整理算法(优化标记 - 清除):
- 标记:同标记 - 清除;
- 整理:将存活对象向内存一端移动,然后清除端外的垃圾对象。
-
优点:无内存碎片;
-
缺点:效率更低(多了整理步骤)。
-
老年代 GC(Full GC):
- 触发条件:老年代空间不足、永久代 / 元空间不足、Minor GC 后存活对象过多无法放入 S 区等;
- 过程:同时回收年轻代和老年代,停顿时间长(STW,Stop The World),是调优重点避免的场景。
4. 主流垃圾收集器(GC 实现)
垃圾收集器是 GC 算法的具体实现,HotSpot 提供多种收集器,需根据业务场景选择(核心关注:吞吐量、停顿时间):
| 收集器 | 适用区域 | 算法核心 | 特点(吞吐量 / 停顿) | 适用场景 |
|---|---|---|---|---|
| Serial GC | 年轻代 | 复制算法(单线程) | 停顿时间长,吞吐量低 | 单线程环境(如桌面应用) |
| ParNew GC | 年轻代 | 复制算法(多线程) | 停顿时间较短,吞吐量一般 | 多线程环境(配合 CMS 使用) |
| Parallel Scavenge | 年轻代 | 复制算法(多线程) | 吞吐量优先,停顿可接受 | 后台计算(吞吐量优先场景) |
| Serial Old GC | 老年代 | 标记 - 整理(单线程) | 停顿时间长 | 单线程环境,或作为应急收集器 |
| Parallel Old GC | 老年代 | 标记 - 整理(多线程) | 吞吐量优先 | 配合 Parallel Scavenge,后台计算 |
| CMS GC | 老年代 | 标记 - 清除(多线程并发) | 停顿时间极短(低延迟) | 互联网应用(响应时间优先) |
| G1 GC | 全堆(不分代) | 区域化 + 复制 + 标记 - 整理 | 低延迟 + 高吞吐量 | 大堆场景(如 8G + 堆内存) |
| ZGC/Shenandoah | 全堆 | 并发回收 + 区域化 | 毫秒级停顿(超低延迟) | 超大堆(如 100G+)、高并发场景 |
推荐组合:
- 吞吐量优先:Parallel Scavenge(年轻代)+ Parallel Old(老年代);
- 低延迟优先:ParNew(年轻代)+ CMS(老年代);
- 大堆 / 高并发:G1 GC(JDK 9 + 默认);
- 超大堆 / 超低延迟:ZGC(JDK 11+)。
四、类加载机制
类加载子系统负责将.class 文件加载到 JVM 内存(方法区),生成 Class 对象(存于堆中),核心流程是 "双亲委派模型"。
1. 类加载的生命周期
类从加载到卸载的完整流程:加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载,其中 "加载 - 验证 - 准备 - 解析 - 初始化" 是类加载的核心阶段。
| 阶段 | 核心操作 |
|---|---|
| 加载 | 通过类全限定名(如java.lang.String)获取.class 文件字节流,转换为方法区的运行时数据结构,生成 Class 对象。 |
| 验证 | 校验.class 文件的合法性(如格式正确、字节码安全),防止恶意字节码攻击。 |
| 准备 | 为类的静态变量分配内存,设置默认初始值(如int默认 0,boolean默认 false),不执行赋值语句。 |
| 解析 | 将常量池中的符号引用(如类名、方法名)转换为直接引用(内存地址)。 |
| 初始化 | 执行类的静态代码块(<clinit>()方法)和静态变量赋值语句,是类加载的最后阶段(只有主动使用时才触发)。 |
2. 双亲委派模型(核心机制)
(1)原理
类加载器加载类时,先委托给父类加载器尝试加载,只有父类加载器无法加载时,才由自身加载(避免类重复加载,保证核心类的安全性)。
(2)类加载器层级(从父到子)
- 启动类加载器(Bootstrap ClassLoader):最顶层,由 C++ 实现,加载 JDK 核心类库(如
JAVA_HOME/jre/lib下的 rt.jar); - 扩展类加载器(Extension ClassLoader):加载 JDK 扩展类库(如
JAVA_HOME/jre/lib/ext目录); - 应用程序类加载器(Application ClassLoader):加载应用程序 classpath 下的类(自己写的代码、第三方 jar 包);
- 自定义类加载器:继承
ClassLoader类,重写findClass()方法,用于加载自定义路径的类(如热部署、加密类)。
(3)流程示例
加载自定义类com.example.User:
- 应用程序类加载器委托给扩展类加载器;
- 扩展类加载器委托给启动类加载器;
- 启动类加载器无法加载(
com.example.User不是核心类),返回给扩展类加载器; - 扩展类加载器无法加载(不在 ext 目录),返回给应用程序类加载器;
- 应用程序类加载器从 classpath 找到
User.class,加载并生成 Class 对象。
(4)破坏双亲委派模型的场景
- 热部署(如 Tomcat 的 WebAppClassLoader,每个 Web 应用独立加载自己的类,避免冲突);
- SPI 机制(如 JDBC,核心类由启动类加载器加载,但驱动类需应用类加载器加载,通过线程上下文类加载器实现)。
五、JVM 调优实践
JVM 调优的核心目标:减少 Full GC 次数、降低 STW 停顿时间、避免 OOM 异常,最终提升系统吞吐量和响应速度。
1. 调优前提:明确目标与监控指标
(1)调优目标
- 吞吐量:CPU 用于执行业务代码的时间占比(如 99%);
- 停顿时间:GC 导致的 STW 时间(如单次 Minor GC < 50ms,Full GC < 1s);
- 内存占用:堆 / 元空间的内存使用合理,无内存泄漏。
(2)核心监控指标
- 堆内存:Eden/Survivor/ 老年代的使用率、GC 次数、GC 耗时;
- 元空间:使用率、是否频繁扩容;
- 线程:线程数、线程栈深度、死锁情况;
- 工具:jps(查看 JVM 进程)、jstat(GC 统计)、jmap(内存快照)、jstack(线程快照)、VisualVM(可视化监控)、Arthas(在线诊断)。
2. 常用调优参数(HotSpot)
(1)堆内存参数(核心)
-Xms2g # 堆初始大小(建议与-Xmx一致)
-Xmx4g # 堆最大大小(避免频繁扩容)
-Xmn1g # 年轻代大小(默认堆的1/3,调整后老年代=堆大小-Xmn)
-XX:SurvivorRatio=8 # Eden区与S区比例(默认8:1,Eden:S0:S1=8:1:1)
-XX:MaxTenuringThreshold=15 # 对象晋升老年代的阈值(默认15)
(2)GC 收集器参数
# 1. 吞吐量优先(Parallel Scavenge + Parallel Old)
-XX:+UseParallelGC # 年轻代使用Parallel Scavenge
-XX:+UseParallelOldGC # 老年代使用Parallel Old
-XX:GCTimeRatio=99 # 吞吐量目标(GC时间占比≤1%)
# 2. 低延迟优先(ParNew + CMS)
-XX:+UseParNewGC # 年轻代使用ParNew
-XX:+UseConcMarkSweepGC # 老年代使用CMS
-XX:CMSInitiatingOccupancyFraction=75 # CMS触发阈值(老年代使用率75%,默认92%)
-XX:+UseCMSCompactAtFullCollection # Full GC后整理内存(解决碎片)
# 3. G1 GC(推荐大堆)
-XX:+UseG1GC # 启用G1
-XX:G1HeapRegionSize=16m # 每个Region大小(1M~32M,2的幂)
-XX:MaxGCPauseMillis=200 # 目标停顿时间(默认200ms)
(3)元空间 / 永久代参数
# JDK 8+ 元空间
-XX:MetaspaceSize=64m # 元空间初始大小(触发Full GC的阈值)
-XX:MaxMetaspaceSize=256m # 元空间最大大小(避免占用过多本地内存)
# JDK 7及以前 永久代
-XX:PermSize=64m
-XX:MaxPermSize=256m
(4)日志与调试参数
-XX:+PrintGCDetails # 打印GC详细日志
-XX:+PrintGCTimeStamps # 打印GC时间戳
-XX:+HeapDumpOnOutOfMemoryError # OOM时生成堆快照(.hprof文件,用于分析)
-XX:HeapDumpPath=/tmp/oom.hprof # 堆快照存储路径
3. 常见问题排查与调优案例
(1)OOM:Java heap space(堆溢出)
- 原因:堆内存不足,对象无法分配(如内存泄漏、堆配置过小);
- 排查:
- 分析堆快照(.hprof 文件),用 MAT(Memory Analyzer Tool)查找大对象 / 内存泄漏(如未关闭的连接、静态集合缓存过多数据);
- 调优:
- 增大堆大小(
-Xmx); - 优化代码,释放无用引用(如关闭数据库连接、清理静态缓存);
- 调整年轻代大小,减少对象晋升老年代的频率。
- 增大堆大小(
(2)频繁 Full GC
- 原因:老年代使用率过高,频繁触发 Full GC(如大对象直接进入老年代、对象晋升阈值过低);
- 排查:
- 用
jstat -gcutil <pid> 1000监控老年代使用率(O 区域),若频繁接近阈值(如 90%+),则说明问题;
- 用
- 调优:
- 增大老年代大小(减少
-Xmn,增加老年代占比); - 调整
-XX:MaxTenuringThreshold,让对象在年轻代多存活一段时间,避免过早晋升; - 避免创建大对象(拆分大对象,或使用
-XX:+UseCompressedOops压缩对象指针)。
- 增大老年代大小(减少
(3)GC 停顿时间过长
- 原因:Full GC 次数多、堆过大导致 GC 遍历时间长、收集器选择不当;
- 调优:
- 更换低延迟收集器(如 CMS/G1/ZGC);
- 减小堆大小(若堆过大,可拆分服务或使用分布式架构);
- 调整 G1 的
-XX:MaxGCPauseMillis,优化 Region 大小; - 减少大对象创建,避免 Full GC 时整理内存的耗时。
六、总结
JVM 的核心是 "内存管理 + 垃圾回收",掌握其内存模型、GC 原理、类加载机制是调优的基础。实际应用中需注意:
- 依据业务场景(吞吐量 / 低延迟)选择合适的 GC 收集器和参数;
- 避免盲目调优,先通过监控工具定位问题(如 OOM、频繁 GC),再针对性优化;
- 优先优化代码(如避免内存泄漏、减少大对象),再调整 JVM 参数;
- 生产环境建议开启 GC 日志和 OOM 堆快照,便于问题排查。
JVM 调优是一个 "迭代优化" 的过程,需结合实际业务压力测试,逐步调整参数,达到最优性能。