深入 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();
    }
}

▶️ 运行步骤

  1. 编译并运行程序

bash

javac MemoryDemo.java
java MemoryDemo
  1. 在另一个终端执行监控命令

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 键的截图。


📸 截图3jstat -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)

📸 截图5OOMDemo.java 完整代码截图。
📸 截图6:OOM 错误终端输出截图(显示 OutOfMemoryError: Java heap space)。
📸 截图7:VS Code 资源管理器显示 .hprof 文件截图(java_pid30252.hprof)。


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

🎯 目标

使用 Eclipse MAT 打开 .hprof 文件,定位内存泄漏根源。

▶️ 操作步骤

  1. 下载并安装 Eclipse MAT(独立版)。

  2. 双击 MemoryAnalyzer.exe 启动。

  3. File → Open Heap Dump,选择 java_pid30252.hprof

  4. 点击 Leak Suspects Report,查看内存泄漏报告。

  5. 导出报告: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:13byte[] 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,导致:

  1. Eden 区迅速填满 → 触发 Young GC

  2. Young GC 无法回收(ArrayList 持有引用)→ 对象晋升到老年代

  3. 老年代填满 → 触发 Full GC

  4. 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 无法回收的对象),但同时也提供了清理它们的选项。具体来说:

  1. MAT 默认行为:加载 .hprof 文件时,会保留所有对象(包括不可达对象),让你可以查看和分析它们。

  2. GC 的时机:在程序运行时,GC 会在适当的时机回收不可达对象。但在生成堆转储的瞬间,有些不可达对象可能还没来得及被 GC 回收。

  3. 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].


🙏 写在最后

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值