jstat 实时监控 · 制造内存溢出 · 堆转储分析 —— 三个实验彻底搞懂 Java 内存模型
目录
思考题2:-Xmx10m 参数的含义是什么?如果不设置,OOM 还会发生吗?
思考题3:什么是不可达对象?为什么 MAT 中还有 948 个不可达对象?
📌 适用人群:Java 开发者、准备面试、想理解 JVM 内存管理的读者
🔥 你将学到:JVM 堆内存结构、垃圾回收机制、jstat 监控、OOM 制造、堆转储分析
📚 一、知识点速览
| 知识点 | 说明 | 重要度 |
|---|---|---|
| JVM 堆内存结构 | 年轻代(Eden/S0/S1)、老年代、元空间 | ⭐⭐⭐⭐⭐ |
| 垃圾回收算法 | 标记-清除、复制、标记-整理、分代收集 | ⭐⭐⭐⭐⭐ |
| 常见 GC 日志 | YGC(Young GC)、FGC(Full GC) | ⭐⭐⭐⭐ |
| 内存溢出(OOM) | 堆内存不足时的典型错误 | ⭐⭐⭐⭐⭐ |
| 堆转储分析 | MAT 工具分析 .hprof 文件 | ⭐⭐⭐⭐ |
| JVM 调优参数 | -Xmx、-XX:+HeapDumpOnOutOfMemoryError | ⭐⭐⭐⭐ |
🧪 二、实验环境准备
-
JDK 26
-
VS Code
-
Eclipse Memory Analyzer Tool (MAT)
-
项目文件夹:
JVMDemo
📸 截图1:VS Code 中 JVMDemo 项目文件夹结构截图
✨ 三、实验一:jstat 实时监控 JVM 堆内存变化
🎯 目标
运行一个不断分配内存的程序,使用 jstat 命令实时观察堆内存变化,理解 GC 的工作过程。
📝 代码文件:MemoryDemo.java
java
import java.util.ArrayList;
import java.util.List;
/**
* 实验一:通过 jstat 观察 JVM 堆内存变化
* 运行后,在另一个终端执行 jstat -gc <PID> 1000
*/
public class MemoryDemo {
public static void main(String[] args) throws Exception {
// 存放分配的字节数组,防止被垃圾回收
List<byte[]> list = new ArrayList<>();
// 获取当前 Java 进程的 PID,供 jstat 使用
long pid = ProcessHandle.current().pid();
System.out.println("当前进程 PID: " + pid);
System.out.println("请在另一个终端执行命令:jstat -gc " + pid + " 1000");
System.out.println("按 Enter 键开始分配内存...");
System.in.read(); // 等待用户按 Enter
// 循环分配内存,每次 1MB,共 100 次
for (int i = 0; i < 100; i++) {
byte[] chunk = new byte[1024 * 1024];
list.add(chunk);
System.out.println("已分配 " + (i + 1) + " MB 内存");
Thread.sleep(300);
}
System.out.println("分配完毕,按 Enter 退出...");
System.in.read();
}
}
▶️ 运行步骤
-
编译并运行程序:
bash
javac MemoryDemo.java
java MemoryDemo
-
在另一个终端执行监控命令:
bash
jstat -gc 13188 1000
✅ 运行结果
终端1(程序运行):
text
当前进程 PID: 13188
请在另一个终端执行命令:jstat -gc 13188 1000
按 Enter 键开始分配内存...
已分配 1 MB 内存
已分配 2 MB 内存
...
已分配 100 MB 内存
分配完毕,按 Enter 退出...
终端2(jstat 监控):
text
S0C S1C EC OC MC YGC FGC
512.0 512.0 2560.0 1024.0 22016.0 2 0
512.0 512.0 2048.0 1024.0 22016.0 3 0
...
📸 截图2:终端显示 PID 和等待 Enter 键的截图。

📸 截图3:jstat -gc 命令输出截图(显示 S0C、EC、OC、YGC 等列)。

📸 截图4:两个终端并列截图(左侧 MemoryDemo 分配内存,右侧 jstat 实时监控,YGC 列逐步增加)。


🔥 四、实验二:制造内存溢出(OOM)并生成堆转储文件
🎯 目标
编写程序不断分配内存,触发 OutOfMemoryError,并生成 .hprof 堆转储文件。
📝 代码文件:OOMDemo.java
java
import java.util.ArrayList;
import java.util.List;
/**
* 实验二:制造内存溢出(OOM)
* 运行参数:-Xmx10m -XX:+HeapDumpOnOutOfMemoryError
*/
public class OOMDemo {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
int count = 0;
try {
while (true) {
byte[] chunk = new byte[1024 * 1024]; // 每次分配 1MB
list.add(chunk);
count++;
System.out.println("已分配 " + count + " MB");
}
} catch (OutOfMemoryError e) {
System.out.println("========== 发生 OOM ==========");
System.out.println("总共分配了 " + count + " MB 内存");
System.out.println("错误信息:" + e.getMessage());
e.printStackTrace();
}
}
}
▶️ 编译运行
bash
javac OOMDemo.java
java -Xmx10m -XX:+HeapDumpOnOutOfMemoryError OOMDemo
✅ 运行结果
text
已分配 1 MB
已分配 2 MB
...
已分配 9 MB
已分配 10 MB
========== 发生 OOM ==========
总共分配了 10 MB 内存
错误信息:Java heap space
java.lang.OutOfMemoryError: Java heap space
at OOMDemo.main(OOMDemo.java:13)
📸 截图5:OOMDemo.java 完整代码截图。
📸 截图6:OOM 错误终端输出截图(显示 OutOfMemoryError: Java heap space)。
📸 截图7:VS Code 资源管理器显示 .hprof 文件截图(java_pid30252.hprof)。
🔍 五、实验三:Eclipse MAT 堆转储分析
🎯 目标
使用 Eclipse MAT 打开 .hprof 文件,定位内存泄漏根源。
▶️ 操作步骤
-
下载并安装 Eclipse MAT(独立版)。
-
双击
MemoryAnalyzer.exe启动。 -
File → Open Heap Dump,选择java_pid30252.hprof。 -
点击
Leak Suspects Report,查看内存泄漏报告。 -
导出报告:
File → Export→ 保存为_Leak_Suspects.zip。
✅ 分析结果
MAT 日志输出:
text
Heap D:\...\java_pid30252.hprof contains 29,657 objects
Removed 948 unreachable objects using 15,904 bytes
Finished opening dump in 1.07 seconds
问题定位:
-
问题代码:
OOMDemo.java:13(byte[] chunk = new byte[1024 * 1024];) -
最大对象:
byte[](占用了 4 MB / 5.3 MB 堆内存) -
泄漏源:
ArrayList不断引用分配的byte[],无法被 GC 回收
📸 截图8:Eclipse MAT 启动后的主界面截图。


📸 截图9:MAT Leak Suspects Report 截图(显示 OOMDemo.main 第13行、byte[] 占用内存、堆栈信息)。



📊 六、原理讲解
6.1 JVM 堆内存结构
| 区域 | 作用 | GC 频率 |
|---|---|---|
| Eden 区 | 新对象分配区 | 频繁 |
| Survivor 0/1 | 存活对象复制区 | 频繁 |
| 老年代 | 长期存活对象 | 较少 |
| 元空间 | 类元数据 | 极少 |
6.2 垃圾回收算法
| 算法 | 原理 | 特点 |
|---|---|---|
| 标记-清除 | 标记存活对象,清除未标记的 | 有内存碎片 |
| 复制 | 存活对象复制到另一半 | 无碎片,内存利用率低 |
| 标记-整理 | 标记存活对象,整理到一端 | 无碎片,移动开销大 |
| 分代收集 | 不同区域用不同算法 | 效率高 |
6.3 OOM 原因分析
本实验中,OOMDemo 每次分配 1MB byte[] 并存入 ArrayList,导致:
-
Eden 区迅速填满 → 触发 Young GC
-
Young GC 无法回收(ArrayList 持有引用)→ 对象晋升到老年代
-
老年代填满 → 触发 Full GC
-
Full GC 仍无法回收 → 抛出
OutOfMemoryError
💡 七、思考题(附答案)
思考题1:为什么 byte[] 会直接分配到堆内存中?
答案:
在 Java 中,所有对象(包括数组)都分配在堆内存(Heap)中。byte[] 是一个数组对象,它本质上也是对象,因此必须存储在堆中。
具体来说:
-
数组本身是对象,拥有对象头(mark word、class pointer)和数组长度信息。
-
数组元素(如
byte数据)存储在数组对象内部连续的内存区域中。 -
无论是
new byte[1024]还是new Object(),只要是new出来的,都会在堆中分配内存。
本实验中:你的 new byte[1024 * 1024] 每次都在堆中分配 1MB 连续内存,且被 ArrayList 引用,无法被 GC 回收,最终导致堆内存耗尽。
思考题2:-Xmx10m 参数的含义是什么?如果不设置,OOM 还会发生吗?
答案:
-Xmx10m 是 JVM 启动参数,含义是:设置 JVM 堆内存的最大值为 10 MB。
-
Xmx=Max Heap Size(最大堆大小) -
10m = 10 MB(
m表示 MB,g表示 GB)
如果不设置 -Xmx:
-
JVM 会使用默认的最大堆大小,这取决于操作系统和 JDK 版本。
-
例如,在大多数 64 位系统上,默认最大堆大小约为物理内存的 1/4 或 512 MB(通常不低于 256 MB)。
OOM 还会发生吗?
-
会的,只是发生得更晚。
-
因为你的程序会不断分配
byte[]直到填满整个堆内存(可能是 256 MB、512 MB 甚至更大)。 -
一旦堆内存耗尽,JVM 就会抛出
OutOfMemoryError。
你可以用
java -XX:+PrintFlagsFinal -version | findstr HeapSize查看默认堆大小。
本实验中:我们设置 -Xmx10m 是为了加速 OOM 的发生,让你在几秒钟内就能看到内存溢出的效果,而不需要等待几分钟或更长时间。
思考题3:什么是不可达对象?为什么 MAT 中还有 948 个不可达对象?
答案:
不可达对象(Unreachable Objects) 是指没有任何活跃引用指向它的对象。换句话说,这些对象不再被程序中的任何变量、数组元素或对象字段引用,因此垃圾回收器(GC)可以(且应该)回收它们。
为什么 MAT 中还有 948 个不可达对象?
因为 MAT 在分析堆转储时,默认会保留不可达对象(以便你检查 GC 无法回收的对象),但同时也提供了清理它们的选项。具体来说:
-
MAT 默认行为:加载
.hprof文件时,会保留所有对象(包括不可达对象),让你可以查看和分析它们。 -
GC 的时机:在程序运行时,GC 会在适当的时机回收不可达对象。但在生成堆转储的瞬间,有些不可达对象可能还没来得及被 GC 回收。
-
MAT 的清理日志:你的 MAT 日志中显示
Removed 948 unreachable objects using 15,904 bytes,说明 MAT 在分析过程中主动清理了这些不可达对象(因为它们对分析没有价值)。
本实验中:
-
那些 948 个不可达对象很可能是程序运行时产生的临时对象(比如循环中的中间变量、迭代器对象等),它们在程序崩溃时还在内存中,但已经没有任何引用指向它们。
-
MAT 检测到它们不可达后,自动清理了它们,以腾出空间供你分析主要的内存泄漏对象(即
byte[])。
📚 参考文献
-
Oracle. Java HotSpot VM Options [Online].
-
Eclipse Foundation. Eclipse Memory Analyzer Tool (MAT) [Online].
-
耿祥义, 张跃平. Java面向对象程序设计(第4版) 第12章 JVM 内存管理.
-
Oracle. Java Garbage Collection Basics [Online].
🙏 写在最后
如果你觉得这篇文章对你有帮助,请点赞👍 + 收藏⭐ + 评论💬 支持一下!
你的鼓励是我持续输出硬核技术文章的动力。

230

被折叠的 条评论
为什么被折叠?



