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


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 概念辨析能力:面试官想确认你是否能准确区分 Memory Leak(内存泄漏)和 Memory Overflow(Memory Overflow / OOM),而不是含糊地说 "都是内存不够"。
  2. 因果关联理解:能否说清楚内存泄漏如何一步步导致内存溢出,以及它们之间不是充分必要条件。
  3. 实战排查经验:是否能结合代码举例说明常见的内存泄漏场景,以及生产环境中如何排查和处理 OOM。

核心答案

直接上对比:

维度 内存泄漏(Memory Leak) 内存溢出(Memory Overflow / OOM)
定义 对象不再使用,但 GC 无法回收(还被人引用着) 程序申请内存时,剩余可用内存不够分配
本质 程序 bug,该释放的没释放 资源耗尽,真的没空间了
发生速度 缓慢积累,像 "慢性病" 瞬间爆发,像 "急性心梗"
因果关系 长期泄漏 → 内存逐渐耗尽 → 最终导致 OOM 也可以由一次分配超大对象直接触发
比喻 水龙头没关紧,水一点点漏 水缸满了,再倒水就溢出来

一句话:内存泄漏是 "该死的不死",内存溢出是 "真的装不下了"。

深度解析

一、内存泄漏(Memory Leak)

内存泄漏指的是:程序中有些对象已经不再被业务逻辑使用了,但由于仍然被某些引用链持有,GC 认为它还 "活着",无法回收。这些对象就像 "僵尸" 一样占着内存不走,越积越多。

如上图所示,内存泄漏的核心在于:业务逻辑上已经不再需要的对象,仍然被某个 "活着" 的引用链(比如一个长生命周期的集合)持有着。GC 通过可达性分析发现这些对象可达,所以不会回收它们,导致它们像 "僵尸" 一样持续占用内存。

二、常见的内存泄漏场景

这块面试官特别爱问,因为能区分出你是 "纸上谈兵" 还是 "踩过坑"。

场景 1:静态集合持有对象引用

public class LeakDemo {
    // 静态集合生命周期 = 应用生命周期,放进去的对象永远不会被回收
    private static final List<Object> CACHE = new ArrayList<>();

    public void process() {
        Object obj = new Object();
        CACHE.add(obj);  // obj 用完后没有从 CACHE 中移除
        // obj 业务上已经不需要了,但 CACHE 还引用着 → 泄漏
    }
}

静态集合的生命周期等同于类加载器的生命周期,基本就是整个应用运行期间。往里面塞对象却不清理,就是最经典的内存泄漏。

场景 2:未关闭的资源

public void readFile(String path) {
    try {
        Connection conn = DriverManager.getConnection(url);
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery("SELECT * FROM users");
        // 处理结果...
        // 忘记 close() → 连接对象泄漏,连接池耗尽
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

数据库连接、IO 流、ThreadLocal 等资源用完不关闭,底层对象不会被 GC 回收。

场景 3:ThreadLocal 忘记 remove()

public class ThreadLocalLeak {
    // 线程池中线程复用,ThreadLocal 不 remove 就会一直累积
    private static final ThreadLocal<byte[]> THREAD_LOCAL = new ThreadLocal<>();

    public void process() {
        THREAD_LOCAL.set(new byte[1024 * 1024]); // 1MB
        // 业务处理...
        // 忘记 THREAD_LOCAL.remove() → 泄漏!
    }
}

线程池中线程是复用的,ThreadLocalremove() 的话,值会一直在线程的 ThreadLocalMap 中挂着,这个坑线上出过不少事故。

场景 4:内部类持有外部类引用

public class Outer {
    private byte[] data = new byte[1024 * 1024]; // 1MB

    class Inner {
        // Inner 隐式持有 Outer.this 引用
        // 即使 Outer 对象不再使用,只要 Inner 还被引用,Outer 也无法回收
    }
}

非静态内部类会隐式持有外部类的引用。如果内部类实例的生命周期比外部类长(比如被放进静态集合或交给异步线程),外部类对象就会泄漏。

三、内存溢出(Memory Overflow / OOM)

内存溢出就直白多了:JVM 在申请分配内存时,堆内存(或其他内存区域)中没有足够的连续空间来分配新对象,就会抛出 OutOfMemoryError

内存溢出有三种典型触发路径:

  • 路径一:泄漏积累导致 OOM。这是最常见的类型。内存泄漏导致僵尸对象越来越多,可用空间越来越少,最终在一次常规的对象分配时触发 OutOfMemoryError: Java heap space
  • 路径二:一次性分配超大对象。比如直接 new byte[Integer.MAX_VALUE],或者一次性查询了几百万条数据库记录加载到内存。这种 OOM 不需要泄漏积累,一次就爆。
  • 路径三:堆内存设置不合理。应用本身需要的内存就比较大,但启动参数 -Xmx 设置得太小,正常负载下也会频繁 OOM。

四、OOM 的常见类型

面试时如果能说出不同类型的 OOM,加分不少:

OOM 类型 触发区域 常见原因
Java heap space Java 堆 对象太多,堆内存不够
Metaspace 元空间(JDK 8+) 动态生成太多 Class(如 CGLIB 代理)
GC overhead limit exceeded Java 堆 GC 回收的量太少,98% 以上时间都在 GC
Direct buffer memory 堆外内存 NIO 的 DirectByteBuffer 分配过多
unable to create new native thread 操作系统 线程数太多,操作系统无法创建新线程
Requested array size exceeds VM limit Java 堆 申请的数组大小超过 JVM 限制

五、两者的因果关系

这里有个关键点:内存泄漏不一定导致 OOM,OOM 也不一定是因为内存泄漏

  • 内存泄漏积累到一定程度 → 导致 OOM ✅
  • 内存泄漏在短时间内没积累到阈值 → 不会 OOM,但可用内存在减少
  • 一次分配超大对象 → OOM,但跟泄漏无关
  • 堆设置太小 → OOM,也不是泄漏

所以面试时如果被问到 "内存泄漏和内存溢出什么关系",标准答案是:内存泄漏是内存溢出的常见原因之一,但不是唯一原因。

六、如何排查?

这块能聊出来就是加分项了,说明你不是只懂理论。

排查内存泄漏

  • jmap -histo:live <pid>:查看存活对象统计,找到数量异常多的类
  • jmap -dump:format=b,file=heap.hprof <pid>:导出堆 dump 文件
  • 用 MAT(Memory Analyzer Tool)或 VisualVM 分析 dump,找 "泄漏嫌疑人"(Leak Suspects)
  • 关注 Dominator Tree(支配树)中占用内存最大的对象和引用链

排查内存溢出

  • 加 JVM 参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dump,OOM 时自动生成 dump 文件
  • jstat -gc <pid> 1000 实时监控各内存区域的使用情况
  • 检查 -Xmx 设置是否合理,是否需要调大堆内存
  • 如果是 Metaspace 溢出,检查是否有大量动态代理生成类的场景

面试高频追问

  1. 怎么排查线上 OOM 问题?

    • 核心思路:加 -XX:+HeapDumpOnOutOfMemoryError 参数,OOM 时自动 dump → 用 MAT 分析 dump 文件 → 找到占用内存最大的对象和引用链 → 定位到具体代码。如果是突发的 OOM,也可以用 jmap 手动导出。
  2. WeakHashMap 能解决内存泄漏吗?

    • 能,但有条件。WeakHashMap 的 key 是弱引用,GC 时如果 key 没有其他强引用就会被回收。适用于 "缓存" 场景——key 不再使用时自动清理。但如果你把 key 作为强引用还在别处持有,那 WeakHashMap 也救不了。
  3. 怎么预防内存泄漏?

    • 几个原则:集合用完清空或置 null;资源用完在 finally 块或 try-with-resources 中关闭;ThreadLocal 用完必须 remove();避免在长生命周期对象中持有短生命周期对象的引用;静态集合慎用,要有清理机制。

常见面试变体

  • "什么是内存泄漏?举个代码例子。"
  • "内存泄漏和内存溢出有什么区别和联系?"
  • "Java 中常见的内存泄漏场景有哪些?"
  • "线上 OOM 怎么排查?"

记忆口诀

区分:泄漏是 "该死不死"(引用没断),溢出是 "装不下了"(空间不够)。 关系:泄漏积累可致溢出,溢出不一定因泄漏。 排查:dump 文件 + MAT 分析,找引用链定代码。

总结

内存泄漏是程序 bug——对象不再使用但引用没断,GC 回收不了;内存溢出是资源耗尽——申请内存时没有足够空间。内存泄漏长期积累是导致 OOM 的常见原因之一,但 OOM 也可能由一次性分配超大对象或堆内存配置不当直接触发。面试时把 "因果但非等价" 的关系讲清楚,再配上排查思路,就很完美了。