1. jmap:命令行魔法
自动垃圾回收固然很好,但即便是 Java 应用也可能“泄漏”(OutOfMemoryError)、运行缓慢,或因内存使用不当而出现意外卡顿。原因可能包括:
- 内存泄漏(memory leaks):本应被回收的对象仍然“存活”。
- 集合中数据量过大。
- 缓存逻辑错误。
- 资源未释放(例如线程、文件)。
你的目标是学会快速定位这些问题。否则,你可能会从程序员变成“缺陷制造者”!
什么是 jmap?
jmap 是 JDK 自带的实用工具。它可以获取正在运行的 Java 进程的内存“快照”(heap dump)。随后可在其它工具中打开该转储,查看哪些对象占用了内存、各有多少、由谁引用等。
Heap dump 就像你堆的照片:包含其中所有对象、它们的类型、关系以及大小。
如何使用 jmap
查找进程 PID
首先需要知道运行你的 Java 应用的进程标识(PID)。可以通过多种方式获取:
通过 jps(JDK 自带的另一个工具):
jps -l
你会看到所有 Java 进程及其 PID 列表。
也可以通过任务管理器(Task Manager)或 Linux 上的 ps。
生成内存转储
jmap -dump:format=b,file=heap.bin <PID>
- format=b —— 二进制格式(适合分析)。
- file=heap.bin —— 保存转储的文件名。
- <PID> —— 进程标识。
示例:
jmap -dump:format=b,file=heap.bin 12345
拿到 heap dump 之后怎么办?
通常会在图形化工具中分析转储(例如 jvisualvm 或 Eclipse MAT),因为手动在二进制文件里找对象属于高难度操作,还有点“自虐”。
jmap 的其他功能
查看内存统计:
jmap -heap <PID>
显示堆信息:大小、所用 GC、状态。
类直方图:
jmap -histo <PID>
显示各类的统计:每种类型对象的数量和总大小。
输出示例:
| # | 对象 | 数量 | 字节 |
|---|---|---|---|
| 1 | |
1200 | 48000 |
| 2 | |
1000 | 32000 |
| 3 | |
200 | 12800 |
这已经能提供线索:如果某种类型的对象突然达到数百万——就值得警惕了。
2. jvisualvm:可视化内存分析器
jvisualvm 是一款随 JDK 提供的图形化程序(在 bin 目录中)。它可以连接本地(有时也能连接远程)Java 进程,实时监控、生成 heap dump、查看内存、线程、GC、CPU 统计,甚至对代码执行进行剖析。
如果你喜欢点点鼠标、看看漂亮的图表——这就是你的工具。
如何启动 jvisualvm
在命令行(或通过 Windows 快捷方式):
jvisualvm
会打开一个窗口,你会看到所有本地 Java 进程的列表。
jvisualvm 的主要功能
实时内存监控
- 在左侧列表中选择一个进程。
- 切换到 Monitor 标签。
- 你会看到 heap、CPU、线程及类数量的使用图表。
示例:

Heap Dump(内存快照)
- 在 Monitor 面板上点击 Heap Dump。
- 几秒后会出现带有转储分析的标签页。
- 你会看到所有类、对象数量及其总大小的列表。
查找“重量级”对象
- 按大小或数量排序。
- 如果发现某个类型(例如 ArrayList 或 String)占用过多内存——就该深入排查。
引用分析(Reference Graph)
- 点击某个类——查看是谁引用该类的对象。
- 可以追溯到根因:为什么对象没有被垃圾回收。
线程分析
- Threads 标签——显示所有线程及其状态。
- 可以发现“卡死”的线程或过于活跃的线程。
性能剖析
- Profiler 标签——衡量哪些方法最耗时或最耗内存。
- 这是更深入的分析,但查找内存泄漏通常只需前面的步骤。
3. 实战:分析一次内存泄漏
带泄漏的示例代码
import java.util.ArrayList;
import java.util.List;
public class MemoryLeakDemo {
// 静态集合——老套路!
private static final List<String> bigList = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1_000_000; i++) {
bigList.add("String number " + i);
if (i % 100_000 == 0) {
System.out.println("已添加: " + i);
Thread.sleep(500); // 留点时间给分析
}
}
System.out.println("完成! 不要关闭应用, 打开 jvisualvm.");
Thread.sleep(600_000); // 10 分钟——用于分析
}
}
发生了什么?
- 我们创建了一个静态列表,并向其中添加了一百万条字符串。
- 添加结束后程序会“挂起”(等待),以便你可以连接分析工具进行检查。
用 jvisualvm 进行分析
- 运行该程序。
- 打开 jvisualvm 并选择该进程。
- 在 Monitor 标签查看内存图表。
- 一切正常时,GC 之后内存会回落。
- 若存在泄漏,内存会持续增长。
- 生成 Heap Dump。
- 查看哪些对象占用最多内存。
- 在我们的示例中——java.util.ArrayList 和 java.lang.String。
- 点击对象——查看是谁引用它。
- 你会看到是静态字段 bigList。
如何修复?
- 删除集合中不需要的元素,限制其增长。
- 当大型对象不再需要时,将引用置空。
- 不要把大型集合放在 static 中,除非它们必须常驻。
4. 其他工具:Eclipse MAT、jconsole
Eclipse Memory Analyzer (MAT)
Eclipse MAT 是免费但功能强大的 heap dump 分析工具。借助它可以看到哪些对象占用内存、哪些对象又在“保留”其他对象——即所谓的 retained set,从而准确定位泄漏所在。
该工具还能生成针对可疑对象(leak suspects)的详细报告,并能从容处理数 GB 级的大型转储。
使用也很简单:用 jmap 或 jvisualvm 生成内存转储,在 MAT 中打开,点击 Leak Suspects Report——即可得到已经高亮潜在问题的报告。
jconsole
jconsole 是一个简单易用的 JVM 实时监控工具。它展示内存使用、CPU 负载、线程数量以及垃圾回收行为。
与更高级的 VisualVM 不同,jconsole 不分析 heap dump,但非常适合快速诊断——当你需要一眼看清应用当前状态时尤为方便。
5. 内存分析中的常见错误
错误 1:给错进程做转储。 一台机器上常常跑着多个 Java 应用,很容易弄错 PID。请先通过 jps 核对,确保分析的是你的程序。
错误 2:在没有泄漏的地方期待看到“泄漏”。 GC 不一定会立刻回收对象——有时只有在 Full GC 之后内存才明显回落。不要慌,如果一次之后内存没有释放——看看趋势。
错误 3:忽视了静态/缓存对象的影响。 许多泄漏源于对象被保存在 static 字段或全局集合中。检查是谁在引用“重量级”对象——往往就是 static。
错误 4:不做动态剖析。 Heap dump 很好,但有时需要观察一段时间内的内存增长(jvisualvm 的图表)。如果内存稳定地增长——就需要进一步排查。
错误 5:没有移除监听器/事件处理器。 如果你订阅了事件却没有取消订阅——即使你不再使用,该对象也会一直“存活”在内存中。
建议:不要害怕尝试这些工具!做转储、看图表、点击对象——只有这样你才能真正“掌握”自己程序的内存,并快速找到问题。
GO TO FULL VERSION