Java 内存泄漏不是“内存没有手动释放”,而是无用对象仍被 GC Roots 引用。本文在独立小进程中制造一个有界实验,生成 Heap Dump,并给出从现象到引用链的分析顺序。
Table of contents
Open Table of contents
在隔离目录运行故障样例
下载堆泄漏样例下载, 先编译,再创建只用于实验的输出目录:
mvn package
mkdir -p diagnostics
java -Xms64m -Xmx64m \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=diagnostics/heap-leak.hprof \
-Xlog:gc*:file=diagnostics/gc.log:time,uptime,level,tags \
-cp target/classes dev.elaine.jvmlab.HeapLeakSample
进程会保留 1 MiB 字节数组,最终抛出 OutOfMemoryError: Java heap space。它只使用 64 MiB 堆,但 Heap Dump 仍可能
包含敏感对象;不要在共享生产目录运行。
如果输出 Could not create the Java Virtual Machine,先检查参数拼写与 JDK 版本。如果 OOM 后没有文件,检查目录存在、
写权限和可用磁盘空间。
先区分 OOM 类型
OutOfMemoryError 是一组资源耗尽结果,不都由 Java 堆泄漏造成:
| 错误片段 | 可能区域 | 优先证据 |
|---|---|---|
Java heap space | Java 堆 | Heap Dump、GC 日志 |
Metaspace | 类元数据 | 类加载日志、NMT |
Direct buffer memory | 直接缓冲区 | NMT、缓冲池指标 |
unable to create native thread | 线程/系统资源 | 线程数、进程限制、容器内存 |
| 进程被 OOMKilled | 容器总内存 | cgroup/平台事件、RSS |
只有 Java 堆 OOM 能直接用 Heap Dump 解释。进程被系统杀死时,JVM 可能没有机会写任何 Dump。
分析 Heap Dump
使用 Eclipse MAT、VisualVM 或团队批准的分析工具打开 heap-leak.hprof,按以下顺序检查:
- 查看 Histogram,确认数量或体积异常的类。
- 查看 Dominator Tree,找到**保留大小(Retained Size)**最大的对象。
- 查看到 GC Roots 的引用路径,确认是谁让对象继续可达。
- 回到源码判断该引用是缓存、监听器、线程本地变量还是生命周期错误。
- 修复后使用相同负载比较增长曲线,而不是只确认“不再立刻 OOM”。
样例中 ArrayList 通过内部数组保留所有 byte[],引用链比“字节数组很多”更接近根因。真实系统还应比较两个时间点的
Heap Dump 或 JFR Object Count 增长,区分稳定缓存和持续泄漏。
Oracle 的内存泄漏排查指南推荐使用 Heap Dump 与 JFR 观察增长对象。
修复后增加回归门槛
为泄漏路径建立长时间或重复调用测试,记录稳定后堆占用、完整 GC 后存活集和对象数量。不要断言一次 GC 后内存必须回到 固定字节数;JVM 会保留已提交堆,测试应观察长期存活趋势。
下一步
阅读CPU 飙高、死锁和长暂停排查,把线程、CPU 与 safepoint 证据串成时间线。