内存泄漏和内存溢出的区别是什么?

#Java#面试八股

内存泄漏和内存溢出的区别是什么?

结论先行:内存泄漏(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 exceeded98% 时间在 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()、监听器成对注册反注册

小结

泄漏是「收不掉」,溢出是「不够用」。定位时先分清是基线抬升还是申请过大,再决定是改代码的引用管理,还是调上限与容量规划。

笔记加载中…