OOM是申请失败,Memory Leak是释放受阻;前者因JVM向OS要内存被拒而抛OutOfMemoryError,后者因强引用阻止GC回收导致内存持续累积。

内存溢出(OOM)和内存泄漏(Memory Leak)看着都跟“内存不够用”有关,但底层机制完全不同:OOM 是 申请失败,Memory Leak 是 释放受阻。
内存溢出:JVM 向 OS 要内存,被拒绝了
当代码执行 new、allocateDirect、加载新类等操作时,JVM 会在底层调用操作系统 API(如 mmap 或 malloc)申请内存。一旦超出以下任一限制,OS 直接拒绝分配,JVM 就抛出 OutOfMemoryError:
-
堆内存超限:存活对象太多,GC 清完仍无足够空闲空间 →
java.lang.OutOfMemoryError: Java heap space -
元空间满:类加载器未卸载,新类的元数据持续写入 →
java.lang.OutOfMemoryError: Metaspace -
直接内存超限:
ByteBuffer.allocateDirect()分配 native 内存,超过-XX:MaxDirectMemorySize或系统剩余物理内存 -
栈空间撑爆:单线程方法调用太深(如递归过深),超过
-Xss设置 → 抛StackOverflowError(虽非 OOM 子类,但属同一类资源耗尽)
内存泄漏:JVM 手里有内存,却不敢还给 GC
GC 是否回收一个对象,只看它是否在 GC Roots 可达路径上。只要本该死亡的对象,被静态变量、ThreadLocal、内部类、监听器等意外持有强引用,它就永远“活着”。JVM 不是不想收,而是规范强制不能收。
-
静态集合缓存不清理:比如
static List<byte[]> cache = new ArrayList<>();持续 add 却 never clear → 所有元素始终被 static 字段引用 -
ThreadLocal 值滞留:在线程池中 set 大对象后没调用
remove()→ ThreadLocalMap 的 Entry 键弱引用,值强引用,复用线程时值一直卡着 -
内部类隐式持外层引用:比如
new Thread(() -> {...}).start()中的 lambda 默认持有 this → 若线程长期运行,外层实例无法释放 - 资源未 close 导致间接泄漏:Connection、InputStream 等未关闭 → 底层 native 句柄或直接内存无法释放,最终可能诱发 OOM
为什么泄漏不报错,却比溢出更难发现?
泄漏本身不会触发异常,程序照常响应请求,但 RSS(进程实际驻留内存)和堆内 used 持续缓慢上涨。GC 日志里会看到 Full GC 频繁、每次回收量却很少——因为那些泄露对象根本不在回收范围内。这种“假空闲”会让监控指标暂时正常,直到某次分配突然失败,才以 OOM 形式爆发。
立即学习“Java免费学习笔记(深入)”;
关键区别一句话总结
OOM 是 JVM 想要新内存,OS 说“没了”;Memory Leak 是 JVM 明知某些内存早该还,却被代码里的强引用锁死,只能眼睁睁看着可用空间越来越少。


















