文章目录
个人网站
线上服务突然挂了,日志里一查: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 exceeded | 堆 | GC 回收太频繁但回收太少,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 回收,持续占用内存。两者的关系是:内存泄露是内存溢出的常见原因——泄露导致可用内存越来越少,最终触发溢出。但溢出不一定是泄露导致的,也可能是业务量超过内存配置。
加分回答
- 提到排查思路:溢出看 dump(HeapDumpOnOutOfMemoryError),泄露看趋势(jmap 定期对比老年代使用量),方法完全不同
- 提到常见泄露源:静态集合、未关闭资源、未注销监听器、ThreadLocal 未 remove——能列出具体场景,说明你真排查过
- 提到预防措施:用 WeakHashMap 替代 HashMap 做缓存、用 try-with-resources 管理资源、线程池中 ThreadLocal 用完必须 remove
面试官点评
这道题考的是你对 JVM 内存管理的理解。如果只说"溢出是内存不够,泄露是内存浪费",太浅了——能讲清楚两者的因果关系、泄露的常见场景和排查方法,才说明你有线上排查经验。如果能区分"溢出不一定是泄露导致的"这个关键点,面试官会认为你思路清晰,不会一看到 OOM 就盲目查泄露,这是加分项。
内容有帮助?点赞、收藏、关注三连!评论区等你 💪


被折叠的 条评论
为什么被折叠?



