目录
- [🟠 34 JVM深入理解](#🟠 34 JVM深入理解)
-
- [1. JVM概述](#1. JVM概述)
-
- [1.1 什么是JVM](#1.1 什么是JVM)
- [1.2 JVM架构](#1.2 JVM架构)
- [1.3 主流JVM](#1.3 主流JVM)
- [2. 内存模型](#2. 内存模型)
-
- [2.1 运行时数据区](#2.1 运行时数据区)
- [2.2 各区域详解](#2.2 各区域详解)
- [2.3 堆内存细分](#2.3 堆内存细分)
- [2.4 对象生命周期](#2.4 对象生命周期)
- [3. 垃圾回收算法](#3. 垃圾回收算法)
-
- [3.1 如何判断对象可回收](#3.1 如何判断对象可回收)
- [3.2 回收算法对比](#3.2 回收算法对比)
- [3.3 分代收集策略](#3.3 分代收集策略)
- [4. 垃圾收集器](#4. 垃圾收集器)
-
- [4.1 收集器一览](#4.1 收集器一览)
- [4.2 G1收集器详解](#4.2 G1收集器详解)
- [4.3 ZGC收集器](#4.3 ZGC收集器)
- [4.4 如何选择收集器](#4.4 如何选择收集器)
- [5. 类加载机制](#5. 类加载机制)
-
- [5.1 类加载过程](#5.1 类加载过程)
- [5.2 双亲委派模型](#5.2 双亲委派模型)
- [5.3 打破双亲委派](#5.3 打破双亲委派)
- [6. JVM调优参数](#6. JVM调优参数)
-
- [6.1 内存参数](#6.1 内存参数)
- [6.2 GC参数](#6.2 GC参数)
- [6.3 常用调优组合](#6.3 常用调优组合)
- [7. GC日志分析](#7. GC日志分析)
-
- [7.1 GC日志格式](#7.1 GC日志格式)
- [7.2 关键指标](#7.2 关键指标)
- [7.3 分析工具](#7.3 分析工具)
- [8. 实战:排查内存问题](#8. 实战:排查内存问题)
-
- [8.1 常见问题](#8.1 常见问题)
- [8.2 排查步骤](#8.2 排查步骤)
- [8.3 内存泄漏示例](#8.3 内存泄漏示例)
- [9. 总结](#9. 总结)
- [📚 参考资料](#📚 参考资料)
🟠 34 JVM深入理解
📅 更新于 2026年6月 | ✍️ 原创文章,转载请注明出处
1. JVM概述
1.1 什么是JVM
JVM(Java Virtual Machine)是Java程序的运行环境,负责将字节码转换为机器码执行。
1.2 JVM架构
┌─────────────────────────────────────────────────────────┐
│ Java Source Code │
└─────────────────────────┬───────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ Java Compiler (javac) │
└─────────────────────────┬───────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ Bytecode (.class) │
└─────────────────────────┬───────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ JVM │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Class Loader │ │ Runtime │ │ Execution │ │
│ │ Subsystem │ │ Data Areas │ │ Engine │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
1.3 主流JVM
| JVM |
厂商 |
特点 |
| HotSpot |
Oracle |
默认JVM,最广泛 |
| OpenJ9 |
Eclipse |
低内存占用 |
| GraalVM |
Oracle |
多语言支持,AOT编译 |
| Zing |
Azul |
低延迟,无停顿 |
2. 内存模型
2.1 运行时数据区
┌─────────────────────────────────────────────────────────┐
│ JVM内存 │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 线程私有 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ 程序计数器│ │ 虚拟机栈 │ │ 本地方法栈 │ │ │
│ │ │ (PC) │ │ (Stack) │ │ (Native Stack)│ │ │
│ │ └──────────┘ └──────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 线程共享 │ │
│ │ ┌──────────────────┐ ┌──────────────────┐ │ │
│ │ │ 堆 (Heap) │ │ 方法区/元空间 │ │ │
│ │ │ 对象实例 │ │ 类信息/常量池 │ │ │
│ │ │ │ │ │ │ │
│ │ │ ┌────┐ ┌────┐ │ │ (Metaspace) │ │ │
│ │ │ │新生代│ │老年代│ │ │ │ │ │
│ │ │ │Eden│ │Old │ │ │ │ │ │
│ │ │ │S0 │ │ │ │ │ │ │ │
│ │ │ │S1 │ │ │ │ │ │ │ │
│ │ │ └────┘ └────┘ │ │ │ │ │
│ │ └──────────────────┘ └──────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
2.2 各区域详解
| 区域 |
线程 |
存储内容 |
异常 |
| 程序计数器 |
私有 |
当前执行的字节码行号 |
无OOM |
| 虚拟机栈 |
私有 |
栈帧(局部变量表、操作数栈) |
StackOverflowError / OOM |
| 本地方法栈 |
私有 |
Native方法 |
StackOverflowError / OOM |
| 堆 |
共享 |
对象实例、数组 |
OOM |
| 方法区/元空间 |
共享 |
类信息、常量、静态变量 |
OOM |
| 直接内存 |
- |
NIO的DirectBuffer |
OOM |
2.3 堆内存细分
堆内存 (Heap)
├── 新生代 (Young Generation) - 1/3 堆空间
│ ├── Eden区 - 8/10 新生代
│ ├── Survivor0 (S0) - 1/10 新生代
│ └── Survivor1 (S1) - 1/10 新生代
│
└── 老年代 (Old Generation) - 2/3 堆空间
2.4 对象生命周期
1. new → Eden区
2. Eden满 → Minor GC → 存活对象 → S0
3. S0满 → Minor GC → 存活对象 → S1
4. 年龄达到阈值(默认15) → 老年代
5. 老年代满 → Major GC / Full GC
3. 垃圾回收算法
3.1 如何判断对象可回收
① 引用计数法
// 每个对象维护引用计数
Object a = new Object(); // 计数=1
Object b = a; // 计数=2
a = null; // 计数=1
b = null; // 计数=0 → 可回收
❌ 问题:循环引用无法回收
② 可达性分析(JVM使用)
GC Roots
│
├── 虚拟机栈中引用的对象
├── 方法区中静态属性引用的对象
├── 方法区中常量引用的对象
├── 本地方法栈中JNI引用的对象
└── 所有被同步锁持有的对象
从GC Roots出发,不可达的对象 → 可回收
3.2 回收算法对比
| 算法 |
原理 |
优点 |
缺点 |
| 标记-清除 |
标记存活,清除未标记 |
简单 |
内存碎片 |
| 标记-整理 |
标记存活,向一端移动 |
无碎片 |
移动开销大 |
| 复制算法 |
分两块,存活对象复制到另一块 |
无碎片,快 |
空间浪费50% |
| 分代收集 |
新生代用复制,老年代用标记-整理 |
综合最优 |
实现复杂 |
3.3 分代收集策略
| 区域 |
算法 |
原因 |
| Eden → S0/S1 |
复制 |
新生代对象朝生夕死,存活率低 |
| 老年代 |
标记-整理 |
对象存活率高,复制浪费空间 |
4. 垃圾收集器
4.1 收集器一览
| 收集器 |
区域 |
算法 |
特点 |
| Serial |
新生代 |
复制 |
单线程,简单高效 |
| ParNew |
新生代 |
复制 |
Serial的多线程版本 |
| Parallel Scavenge |
新生代 |
复制 |
吞吐量优先 |
| Serial Old |
老年代 |
标记-整理 |
单线程 |
| CMS |
老年代 |
标记-清除 |
低延迟(已废弃) |
| Parallel Old |
老年代 |
标记-整理 |
吞吐量优先 |
| G1 |
全堆 |
分区 |
JDK9默认,平衡吞吐和延迟 |
| ZGC |
全堆 |
着色指针 |
超低延迟(<10ms) |
| Shenandoah |
全堆 |
转发指针 |
超低延迟 |
4.2 G1收集器详解
G1将堆划分为多个大小相等的Region(1-32MB)
┌─────┬─────┬─────┬─────┬─────┬─────┐
│ E │ S │ O │ O │ H │ E │
│ │ │ │ │ │ │
├─────┼─────┼─────┼─────┼─────┼─────┤
│ O │ E │ H │ O │ E │ S │
│ │ │ │ │ │ │
├─────┼─────┼─────┼─────┼─────┼─────┤
│ E │ O │ O │ E │ O │ E │
└─────┴─────┴─────┴─────┴─────┴─────┘
E=Eden S=Survivor O=Old H=Humongous(大对象)
G1特点:
- 可预测的停顿时间(
-XX:MaxGCPauseMillis=200)
- 优先回收垃圾最多的Region(Garbage First)
- JDK9+默认收集器
4.3 ZGC收集器
ZGC目标:停顿时间 < 10ms,不随堆大小增长
特点:
- 着色指针(Colored Pointers)
- 读屏障(Load Barrier)
- 并发执行几乎所有阶段
- 支持TB级堆内存
- JDK15+正式可用
启用:-XX:+UseZGC
4.4 如何选择收集器
| 场景 |
推荐收集器 |
| 小堆(<4GB) |
Serial / Parallel |
| 中等堆,吞吐量优先 |
Parallel + Parallel Old |
| 中等堆,延迟敏感 |
G1 |
| 大堆(>8GB),低延迟 |
ZGC |
| 超大堆(>100GB) |
ZGC / Shenandoah |
5. 类加载机制
5.1 类加载过程
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
读取 校验 分配 符号 执行
字节码 格式 内存 引用 <clinit>
(零值) 替换 方法
5.2 双亲委派模型
┌─────────────────┐
│ Bootstrap │ ← 核心类(rt.jar)
│ ClassLoader │
└────────┬────────┘
│ 委派
┌────────▼────────┐
│ Extension │ ← 扩展类(ext/)
│ ClassLoader │
└────────┬────────┘
│ 委派
┌────────▼────────┐
│ Application │ ← 应用类(classpath)
│ ClassLoader │
└────────┬────────┘
│ 委派
┌────────▼────────┐
│ Custom │ ← 自定义类
│ ClassLoader │
└─────────────────┘
加载流程:向上委派 → 找不到 → 向下加载
5.3 打破双亲委派
// 方式1:重写loadClass方法
public class MyClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 自定义加载逻辑
if (name.startsWith("com.myapp")) {
return findClass(name);
}
return super.loadClass(name);
}
}
// 方式2:线程上下文类加载器
// SPI机制、Tomcat类加载、OSGi等
ClassLoader loader = Thread.currentThread().getContextClassLoader();
6. JVM调优参数
6.1 内存参数
| 参数 |
说明 |
示例 |
-Xms |
初始堆大小 |
-Xms512m |
-Xmx |
最大堆大小 |
-Xmx2g |
-Xmn |
新生代大小 |
-Xmn256m |
-Xss |
栈大小 |
-Xss512k |
-XX:MetaspaceSize |
元空间初始大小 |
-XX:MetaspaceSize=256m |
-XX:MaxMetaspaceSize |
元空间最大大小 |
-XX:MaxMetaspaceSize=512m |
-XX:NewRatio |
老年代:新生代比例 |
-XX:NewRatio=2 (2:1) |
-XX:SurvivorRatio |
Eden:Survivor比例 |
-XX:SurvivorRatio=8 (8:1:1) |
6.2 GC参数
| 参数 |
说明 |
示例 |
-XX:+UseG1GC |
使用G1 |
JDK9+默认 |
-XX:+UseZGC |
使用ZGC |
JDK15+ |
-XX:MaxGCPauseMillis |
最大GC停顿时间 |
-XX:MaxGCPauseMillis=200 |
-XX:GCTimeRatio |
GC时间占比 |
-XX:GCTimeRatio=99 (1%时间给GC) |
-XX:+PrintGCDetails |
打印GC详情 |
调试用 |
-Xlog:gc* |
GC日志(JDK9+) |
-Xlog:gc*:file=gc.log |
6.3 常用调优组合
# 开发环境
java -Xms256m -Xmx512m -XX:+PrintGCDetails -jar app.jar
# 生产环境(G1)
java -Xms4g -Xmx4g -XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:G1HeapRegionSize=8m \
-Xlog:gc*:file=gc.log:time,uptime,level,tags \
-jar app.jar
# 低延迟场景(ZGC)
java -Xms8g -Xmx8g -XX:+UseZGC \
-Xlog:gc*:file=gc.log \
-jar app.jar
7. GC日志分析
7.1 GC日志格式
# JDK 8格式
[GC (Allocation Failure) [PSYoungGen: 65536K->10240K(76288K)]
65536K->10280K(251392K), 0.0123456 secs]
# JDK 9+格式
[0.123s][info][gc] GC(0) Pause Young (Allocation Failure)
64M->10M(256M) 12.345ms
7.2 关键指标
| 指标 |
说明 |
健康范围 |
| GC频率 |
每分钟GC次数 |
Young<1次/秒, Full<1次/小时 |
| GC耗时 |
单次GC时间 |
Young<50ms, Full<1s |
| 吞吐量 |
非GC时间占比 |
>95% |
| 晋升速率 |
对象进入老年代速度 |
越低越好 |
7.3 分析工具
| 工具 |
说明 |
jstat |
命令行统计 |
jmap |
堆转储 |
jvisualvm |
可视化监控 |
| GCViewer |
GC日志分析器 |
| GCEasy |
在线GC分析 (gceasy.io) |
# jstat查看GC情况
jstat -gcutil <pid> 1000
# 输出示例
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 50.23 45.67 32.15 95.42 92.31 125 1.234 2 0.456 1.690
8. 实战:排查内存问题
8.1 常见问题
| 问题 |
症状 |
排查方法 |
| 内存泄漏 |
OOM,堆持续增长 |
堆转储分析 |
| 频繁Full GC |
应用卡顿 |
GC日志分析 |
| 线程死锁 |
应用挂起 |
jstack |
| CPU飙升 |
响应慢 |
top + jstack |
8.2 排查步骤
# 1. 查看GC情况
jstat -gcutil <pid> 1000
# 2. 如果频繁Full GC,生成堆转储
jmap -dump:format=b,file=heap.hprof <pid>
# 3. 使用MAT或jvisualvm分析
# 查找占用内存最多的对象
# 查找对象的引用链
# 4. 查看线程状态
jstack <pid> > threads.txt
# 5. 查看CPU使用
top -Hp <pid> # 找到CPU最高的线程
printf "%x\n" <tid> # 转16进制
jstack <pid> | grep <tid_hex> # 找到对应线程
8.3 内存泄漏示例
// ❌ 错误:静态集合不断添加对象
public class MemoryLeak {
private static final List<Object> list = new ArrayList<>();
public void add(Object obj) {
list.add(obj); // 永远不会被GC
}
}
// ❌ 错误:未关闭资源
public void readFile() {
InputStream is = new FileInputStream("file.txt");
// 忘记close,导致资源泄漏
}
// ✅ 正确:使用try-with-resources
public void readFile() {
try (InputStream is = new FileInputStream("file.txt")) {
// 自动关闭
}
}
// ❌ 错误:ThreadLocal未清理
private static ThreadLocal<User> userHolder = new ThreadLocal<>();
public void process() {
userHolder.set(getUser());
// 线程复用时,旧值不会被清理
}
// ✅ 正确:用完清理
public void process() {
try {
userHolder.set(getUser());
// ... 业务逻辑
} finally {
userHolder.remove();
}
}
9. 总结
| 知识点 |
核心内容 |
| 内存模型 |
堆、栈、方法区、程序计数器 |
| GC算法 |
标记-清除、标记-整理、复制、分代 |
| 收集器 |
G1(默认)、ZGC(低延迟) |
| 类加载 |
双亲委派、自定义ClassLoader |
| 调优 |
-Xms/-Xmx、GC参数、日志分析 |
| 排查 |
jstat、jmap、jstack、MAT |
💬 你遇到过JVM内存问题吗?用什么工具排查的?分享一下你的经验!
📌 下一篇我们将学习 Maven项目管理,掌握Java项目的构建与依赖管理!
📚 参考资料