OOM是申请失败,JVM向操作系统要内存被拒;Memory Leak是释放失败,对象被强引用持有导致GC不敢回收,内存持续累积最终可能引发OOM。

OOM 是 JVM 向操作系统要内存时被拒绝;Memory Leak 是 JVM 自己手里有内存,却因为引用没断,死活不敢还给 GC。
OOM 的本质是“申请失败”
当 JVM 执行 new、allocateDirect 或加载类等操作时,底层会向操作系统发起内存分配请求。一旦该请求超出限制(如 -Xmx、-XX:MaxMetaspaceSize、-XX:MaxDirectMemorySize 或系统剩余物理内存),操作系统直接返回失败,JVM 就抛出 OutOfMemoryError。
- 堆溢出:
OutOfMemoryError: Java heap space→ GC 已尽力回收,但存活对象太多,空闲堆空间仍 - 元空间溢出:
OutOfMemoryError: Metaspace→ 类加载器未卸载,新类元数据不断写入,元空间满 - 直接内存溢出:
OutOfMemoryError: Direct buffer memory→ByteBuffer.allocateDirect()调用触发 native 内存分配,超过-XX:MaxDirectMemorySize或系统可用内存 - 栈溢出:
StackOverflowError(虽非 OOM 子类,但同属内存耗尽)→ 单线程栈帧深度超-Xss限制,不是 GC 问题,是线程私有空间撑爆
Memory Leak 的本质是“释放受阻”
GC 能否回收一个对象,只看它是否在 GC Roots 可达路径上。只要某个本该死亡的对象,仍被静态变量、ThreadLocal、监听器、缓存等意外持有强引用,它就始终“活着”,JVM 明知它无用,也绝不敢回收——这不是 JVM 懒,是规范强制要求。
- 静态集合泄露:
static List不清理 → 对象永远被cache = new ArrayList() cache引用,GC Roots 包含该静态字段 - ThreadLocal 泄露:
ThreadLocal在线程池中 set 后不local = new ThreadLocal() remove()→ThreadLocalMap中的Entry键为弱引用,值却强引用着大对象,线程复用后值长期滞留 - 内部类隐式引用:
new Thread(() -> { doSomething(); }).start()中的 lambda 持有外部类 this → 若线程生命周期远长于外部实例,this 就无法回收 - 资源未关闭:未调用
Connection.close()或InputStream.close()→ 底层 native 句柄未释放,可能间接导致直接内存或文件描述符泄漏,最终诱发 OOM
为什么 Leak 常被误认为 OOM 的“前兆”?
Leak 本身不报错,程序照常运行,但 RSS(进程实际驻留内存)和堆内 used 持续缓慢上涨。JVM 看似“还有空闲”,其实是假象——那些被泄露对象占着的内存,GC 永远不会碰。直到某次分配时,剩余可回收空间
最典型的混淆点:OutOfMemoryError: Java heap space 日志里看不到“泄露”字样,但它背后可能是 static Map 三年没清过期 key,也可能是 Spring Context 重启后旧 Bean 仍被监听器引用。定位必须靠堆转储(heap dump)+ 分析对象引用链,不能只看异常类型。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











