孤舟笔记 基础篇十六 程序崩了是溢出还是泄露?它俩到底啥关系啥区别


个人网站

线上服务突然挂了,日志里一查:java.lang.OutOfMemoryError。你跟同事说"内存泄露了",同事说"这是内存溢出"。你俩争了半天,谁也说服不了谁。

到底谁是内存溢出?谁是内存泄露?它俩是一回事吗?不是。但它俩关系密切,理解错就容易排查错方向。今天咱就把这对"难兄难弟"彻底掰扯清楚。

一、先说结论:溢出是结果,泄露是原因

维度内存溢出(OutOfMemory)内存泄露(Memory Leak)
本质内存不够用了,装不下新的对象不再使用的对象没法被回收,内存被占着
时机瞬间爆发,程序直接崩慢性消耗,像温水煮青蛙
现象OutOfMemoryError程序还能跑,但内存越用越多
关系溢出是结果泄露是常见原因
比喻水池满了,水漫出来了水池堵了,脏水排不出去

一句话记住:泄露是"占着茅坑不拉屎",溢出是"茅坑满了没坑位"。泄露久了,必然溢出。

二、内存溢出:装不下了,炸了

1. 什么是内存溢出?

JVM 给每块内存区域都设了上限。当某块区域真的塞满了,再申请空间就抛 OutOfMemoryError,程序直接崩。

// 堆溢出:不断创建对象,塞满堆
List<byte[]> list = new ArrayList<>();
while (true) {
    list.add(new byte[1024 * 1024]);  // 每次塞 1MB
}
// 💥 java.lang.OutOfMemoryError: Java heap space

2. 常见的溢出类型

错误信息溢出区域常见原因
Java heap space对象太多,堆不够用
Metaspace元空间加载的类太多(如动态代理疯狂生成类)
unable to create new native thread线程栈线程数太多,系统资源耗尽
GC overhead limit exceededGC 回收太频繁但回收太少,JVM 放弃了
Direct buffer memory堆外内存NIO 的 ByteBuffer 用多了

溢出不一定是因为泄露——可能就是业务量太大、内存配太小。就好比一个正常的水池,你硬要往里灌十倍的水,那当然漫出来,不是水池坏了,是你水太多了。

3. 怎么排查?

加 JVM 参数让溢出时自动 dump 内存快照:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dump.hprof

然后用 MAT 或 VisualVM 分析 dump 文件,找出谁占了大头。

三、内存泄露:偷偷占着,还不还

1. 什么是内存泄露?

对象已经不再使用了,但 GC 没法回收它——因为还有引用链指向它。这些"僵尸对象"占着内存不干活,越攒越多,最终导致溢出。

public class LeakDemo {
    // 静态集合,生命周期跟类一样长
    static List<Object> cache = new ArrayList<>();

    public void process() {
        Object data = loadData();
        cache.add(data);  // 👈 加进去了,但再也没移除
        // data 用完后没人管了,但 cache 一直引用着它,GC 回收不了
    }
}

data 用完后本该被回收,但 cache 还死死拽着它。一次两次没事,调个几万次,内存就被这些"僵尸"吃光了。

2. 常见的泄露场景

场景一:静态集合当缓存

static Map<String, User> userCache = new HashMap<>();
// 不断往里 put,从不 remove → 泄露

静态集合的生命周期跟类一样长,对象放进去不拿出来,就永远回收不了。

场景二:未关闭的资源

InputStream is = new FileInputStream("data.txt");
// 忘了 is.close() → 泄露

数据库连接、IO 流、网络连接不关,底层资源不会被释放。

场景三:监听器/回调没注销

button.addActionListener(listener);
// 页面销毁时没移除 listener → 泄露

注册了监听器但没注销,发布者还持有监听器的引用,监听器又持有页面/组件的引用,整条链路都回收不了。

场景四:ThreadLocal 用完没 remove

threadLocal.set(userContext);
// 线程复用时没 remove → 泄露

线程池中的线程会被复用,ThreadLocal 的值跟着线程走,用完不清理,下次任务还能读到脏数据,而且对象一直被引用着回收不了。

3. 怎么排查?

jmap 定期查看内存使用趋势:

jmap -heap <pid>       # 查看堆概况
jmap -histo <pid>      # 查看对象数量排行

如果老年代使用量只涨不降,GC 后也不怎么回落,大概率有泄露。再结合 dump 文件分析引用链,找到谁拽着垃圾不放。

四、它俩的关系:泄露是慢性病,溢出是急症

内存泄露(慢性)              内存溢出(急性)
──────────────────────────────────────────
对象占着不还                  内存彻底不够
温水煮青蛙                   突然炸裂
可能持续数天/数周              瞬间崩溃
GC 后内存仍持续上涨           抛 OutOfMemoryError

关系:泄露 → 内存逐渐被占满 → 最终溢出

注意:溢出不一定是泄露导致的
      (可能就是内存配小了、流量太大了)
      泄露不一定会马上溢出
      (但不管它,迟早溢出)

口诀:泄露是占着不还,溢出是装不下了;泄露是慢性病,溢出是急症;慢性病不治,迟早变急症。

五、回答技巧与点评

标准回答

内存溢出是指 JVM 在申请内存时没有足够的空间可供分配,抛出 OutOfMemoryError,程序崩溃。内存泄露是指程序中不再使用的对象无法被 GC 回收,持续占用内存。两者的关系是:内存泄露是内存溢出的常见原因——泄露导致可用内存越来越少,最终触发溢出。但溢出不一定是泄露导致的,也可能是业务量超过内存配置。

加分回答

  1. 提到排查思路:溢出看 dump(HeapDumpOnOutOfMemoryError),泄露看趋势(jmap 定期对比老年代使用量),方法完全不同
  2. 提到常见泄露源:静态集合、未关闭资源、未注销监听器、ThreadLocal 未 remove——能列出具体场景,说明你真排查过
  3. 提到预防措施:用 WeakHashMap 替代 HashMap 做缓存、用 try-with-resources 管理资源、线程池中 ThreadLocal 用完必须 remove

面试官点评

这道题考的是你对 JVM 内存管理的理解。如果只说"溢出是内存不够,泄露是内存浪费",太浅了——能讲清楚两者的因果关系、泄露的常见场景和排查方法,才说明你有线上排查经验。如果能区分"溢出不一定是泄露导致的"这个关键点,面试官会认为你思路清晰,不会一看到 OOM 就盲目查泄露,这是加分项。

原文阅读


内容有帮助?点赞、收藏、关注三连!评论区等你 💪

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值