内存泄漏和内存溢出的区别是什么?
结论先行:内存泄漏(memory leak)是对象已经没用了,却还被引用链持有,垃圾回收器收不走——它是原因;内存溢出(out of memory)是可用内存不够分,申请失败抛 OutOfMemoryError——它是结果。泄漏会持续累积最终演化为溢出,但溢出也可能与泄漏无关,例如一次性申请超大数组或堆上限配得太小。
一、概念对比
| 维度 | 内存泄漏 | 内存溢出 |
|---|---|---|
| 本质 | 该回收的对象回收不掉 | 内存不够用 |
| 表现 | 堆占用持续爬升,Full GC 后仍不回落 | 抛 OutOfMemoryError,进程可能直接退出 |
| 触发条件 | 引用被长生命周期容器持有 | 申请量超过可用上限 |
| 是否必然 OOM | 不一定,泄漏速率慢时可能一直不到上限 | 必然,除非能释放或扩容 |
| 排查重点 | 引用链、容器生命周期 | 申请来源、上限配置、大对象 |
时间 →
堆占用
│ ╱╱╱╱╱╱ ← 泄漏:每次 GC 后基线抬升,锯齿上移
│ ╱╱╱
│ ╱╱
│ ──╱──────────────── 正常:锯齿围绕固定基线波动
│
└────────────────────→ 到 -Xmx 上限即 OutOfMemoryError
二、四种典型泄漏写法
public class LeakCases {
// 1. 静态集合只增不减:生命周期与类相同,等价于永久持有
private static final Map<String, byte[]> CACHE = new HashMap<>();
public void putCache(String key, byte[] data) {
CACHE.put(key, data); // 缺少淘汰策略,等于把堆当硬盘
}
// 2. 监听器/回调注册后不反注册
public void register(EventBus bus) {
bus.register(this); // 若 this 被回收前未 unregister,事件总线长期持有它
}
// 3. 线程池任务里捕获大对象
public void submit(ExecutorService pool, byte[] payload) {
pool.submit(() -> {
byte[] copy = payload; // 任务对象被队列持有期间,payload 无法回收
System.out.println(copy.length);
});
}
// 4. 自定义栈只增不缩:pop 后仍保留数组引用
static class Stack {
private Object[] items = new Object[1024];
private int size;
Object pop() {
return items[--size]; // 槽位仍指向对象,应置 null
}
}
}
ThreadLocal 的泄漏属于第 1 类的变体:ThreadLocalMap 的 key 是弱引用但 value 是强引用,线程池里的工作线程长期存活,value 就跟着泄漏,所以用完必须 remove()。
三、溢出的常见类型与触发点
| OOM 消息 | 含义 | 常见原因 |
|---|---|---|
Java heap space | 堆内对象分配失败 | 泄漏累积、一次性加载大文件、堆上限偏小 |
GC overhead limit exceeded | 98% 时间在 GC 却只回收不到 2% | 堆几乎被存活对象占满 |
Metaspace | 元空间耗尽 | 动态生成类、热部署反复加载、反射膨胀 |
Direct buffer memory | 堆外直接内存超限 | Netty 未释放 ByteBuf、allocateDirect 无上限 |
unable to create new native thread | 线程数触顶 | 线程池无界、每请求一线程、ulimit 限制 |
Requested array size exceeds VM limit | 单次申请数组过大 | new byte[Integer.MAX_VALUE - 2] |
四、排查动作
# 1. 先看曲线:是「一直涨」还是「到点就炸」
jstat -gcutil 1234 1000 30 # 关注 O(老年代使用率)与 FGC 次数
# 2. 现场保留:加 dump 参数重启,或对存活进程主动导
jcmd 1234 GC.heap_dump /tmp/heap.hprof
# 3. 直方图看谁在涨(不必 dump 也能看个大概)
jmap -histo:live 1234 | head -20
# 4. 用 Eclipse MAT 打开 hprof,看 Dominator Tree 与 Path to GC Roots
判断顺序:先确认内存增长与业务量是否成正比,再用 jmap -histo:live 对比两次快照,最后在 MAT 里顺着「最大保留集对象」找引用链。
五、常见追问
| 追问 | 回答要点 |
|---|---|
| 泄漏一定导致 OOM 吗 | 不一定,泄漏速度慢或对象小,可能只表现为 Full GC 变频繁、延迟升高 |
| Full GC 后内存回落说明什么 | 说明没有泄漏或泄漏很轻,属于「分配速率过高」而非「回收不掉」 |
| 堆外泄漏怎么看 | Native Memory Tracking:-XX:NativeMemoryTracking=summary 配 jcmd VM.native_memory |
| OOM 之后进程还能用吗 | 取决于现场:线程创建失败或直接内存失败可能仍存活,但状态不可信,应尽快重启并保留 dump |
| 怎么从设计上避免 | 容器设容量上限、缓存必须有淘汰策略、ThreadLocal 配 remove()、监听器成对注册反注册 |
小结
泄漏是「收不掉」,溢出是「不够用」。定位时先分清是基线抬升还是申请过大,再决定是改代码的引用管理,还是调上限与容量规划。