3. 堆内存溢出(Java heap space)详解
堆内存溢出,说白了就是JVM的堆(Heap)装不下新对象了。
我做了十几年性能调优,遇到最多的OOM就是它。很多人以为只要加大堆内存就能解决,结果呢?照旧崩。为啥?因为你根本不知道到底谁把堆撑爆了。
这节课,我带你把堆内存溢出的根因刨清楚,再手把手演示怎么用工具定位。嗯,咱们直接上干货。
3.1 堆内存溢出到底是怎么发生的?
堆是Java对象的大本营。new出来的对象、数组,全扔这里。GC(垃圾回收)一直在清理没用的对象。但当你创建对象的速度 > GC回收的速度,堆就慢慢涨上去。涨到设置的最大值(-Xmx),GC已经拼了命但还腾不出空间,JVM就会抛出:
java.lang.OutOfMemoryError: Java heap space
我遇到过一位同事,跑一个批处理程序,把整个数据库的表都读到内存里做聚合。堆只有2GB,数据量却几十GB,结果你猜怎么着?跑了3小时,最后OOM,前功尽弃。这就是典型的数据量远超堆容量。
常见的推手有这么几类:
- 对象创建太猛——比如高并发下单,每个请求都new大对象。
- 集合积累无度——往List里塞数据,塞到内存爆炸。
- 大对象直接进老年代——比如几百兆的byte[]。
- 内存泄漏——对象被无意中引用,GC无法回收。
- 堆配置太小——默认值往往只有物理内存的1/4,对于现在的应用经常不够。
3.2 实战复现:写一段必溢出代码
光说不练假把式。我来写个小Demo,你能直接跑出Java heap space:
public class HeapOOMDemo {
public static void main(String[] args) {
// 我故意留一个静态引用,防止GC回收
List<byte[]> list = new ArrayList<>();
while (true) {
// 每次分配10MB
list.add(new byte[10 * 1024 * 1024]);
System.out.println("已分配: " + list.size() + " 个10MB对象");
}
}
}
运行前设置JVM参数:-Xms128m -Xmx128m。控制台会打印“已分配: 1”、“已分配: 2”……直到某个数字后抛出OOM。你猜能分配几个?大约12个左右(128MB / 10MB ≈ 12.8,再算上JVM自身开销)。
3.3 用jmap + dump精准定位
光知道溢出不够,你得找到哪个对象占最多。我个人最常用的工具就是jmap。
3.4.1 生成堆转储文件(Heap Dump)
在OOM发生时,加一句JVM参数让它自动dump:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof
或者手动用jmap抓(进程未挂时):
jmap -dump:live,format=b,file=heap.hprof <pid>
:live参数会先触发Full GC,只保留存活对象。你想看看搞事的是谁,建议先用这个,因为死对象在GC后基本没了,留下来的才是占用内存的真正元凶。
3.4.2 用Eclipse MAT分析dump文件
拿到.hprof文件后用MAT(Memory Analyzer Tool)打开。我习惯先看Leak Suspects,它自动识别溢出嫌疑。通常你会看到类似这样的结论:“The thread main creates 12 instances of byte[]. The total consumption is 120 MB.”——这就是上面Demo的真实写照。
再看Dominator Tree,它能告诉你对象间的引用链,谁是真正撑起树的根。
| MAT关键视图 | 用途 |
|---|---|
| Leak Suspects | 自动检测最可能的泄漏源 |
| Dominator Tree | 按保留堆大小排序,展示引用结构 |
| Histogram | 按类统计对象数量和占用内存 |
| Thread Overview | 查看每个线程的栈和本地变量 |
HashMap或ArrayList,十有八九是集合泄漏。我曾经帮一个电商团队找出线上OOM,就是用MAT看到com.sun.jmx.mbeanserver.JmxMBeanServer持有了所有JMX对象……最后发现是监控框架写错了注册逻辑。
3.5 什么时候该加堆,什么时候该改代码?
我见过不少开发兄弟,一看到OOM就加-Xmx。加完短期不崩了,过两周又崩。你看,这其实是个误区。我们需要分场景:
- 流量高峰期临时溢出:适当加大堆内存,同时检查是否需要限流或扩容。
- 内存泄漏:加再多堆也白搭,早晚耗光。必须修代码。
- 单次数据量极大:比如读取超大文件,考虑流式处理,别全塞内存。
- 缓存太多:用Guava Cache或Caffeine设置最大条目和过期策略,别让它无限增长。
本章知识脑图
好了,这是堆内存溢出最核心的内容。你只要掌握:为什么会溢 → 怎么写代码复现 → 怎么用dump定位,以后线上再遇到Java heap space,就知道从哪里下手了。
顺手总结一下定位流程:
- 加JVM参数
-XX:+HeapDumpOnOutOfMemoryError - 等OOM后取得
java_pidxxx.hprof - 用MAT打开,看Leak Suspects + Dominator Tree
- 找到最大对象,顺引用链定位到代码
- 问自己:这个对象该不该长期持有?能不能弱引用?能不能分批处理?
-XX:+HeapDumpOnOutOfMemoryError自动方式。