欢迎光临
我们一直在努力

深入 JVM 内存管理:从堆结构到垃圾回收

jstat 实时监控 · 制造内存溢出 · 堆转储分析 —— 三个实验彻底搞懂 Java 内存模型

目录

📚 一、知识点速览

🧪 二、实验环境准备

✨ 三、实验一:jstat 实时监控 JVM 堆内存变化

🎯 目标

📝 代码文件:MemoryDemo.java

▶️ 运行步骤

✅ 运行结果

🔥 四、实验二:制造内存溢出(OOM)并生成堆转储文件

🎯 目标

📝 代码文件:OOMDemo.java

▶️ 编译运行

🔍 五、实验三:Eclipse MAT 堆转储分析

🎯 目标

▶️ 操作步骤

✅ 分析结果

📊 六、原理讲解

6.1 JVM 堆内存结构

6.2 垃圾回收算法

6.3 OOM 原因分析

💡 七、思考题(附答案)

思考题1:为什么 byte[] 会直接分配到堆内存中?

思考题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].


    🙏 写在最后

    如果你觉得这篇文章对你有帮助,请点赞👍 + 收藏⭐ + 评论💬 支持一下! 你的鼓励是我持续输出硬核技术文章的动力。

    赞(0)
    未经允许不得转载:171主机测评 » 深入 JVM 内存管理:从堆结构到垃圾回收
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址