内存泄漏是“该走的没走”,对象仍被gc roots可达但业务已无用;内存溢出是“想进进不来”,申请内存时堆空间不足直接oom。前者为慢性积累,后者为瞬时失败。
内存泄漏和内存溢出不是同一类问题,关键区别不在“有没有内存”,而在于“谁在占着、为什么占着、能不能腾”。从jvm堆内存结构图切入,能一眼看清本质。
堆里“该走的没走”——内存泄漏的本质
堆内存中,对象是否被回收,只看它是否还能从GC Roots(如线程栈帧、静态变量、本地方法栈等)被可达访问。内存泄漏的对象,恰恰是可达但无用的:
- 它们还在GC Roots引用链上(比如被static Map长期持有、被未注销的监听器强引用)
- 业务逻辑早已不再使用它们,但JVM无法判定“无用”,只能默认“有用”
- 这些对象堆积在老年代或年轻代,随时间推移不断膨胀,但不会立刻报错
就像堆内存结构图中,Eden区不断分配新对象,Survivor区来回复制,而老年代里却悄悄塞满了一堆“没人再调用、却一直被静态容器拽着”的对象——这就是泄漏的视觉化表现。
堆顶“想进进不来”——内存溢出的本质
内存溢出(OOM)不是堆里“满了就停”,而是申请时刻失败:当JVM尝试在堆中为新对象分配空间时,发现无论怎么触发GC(包括Full GC),都腾不出足够连续/可用空间,就会直接抛出OutOfMemoryError: Java heap space。
- 可能因为堆本身设得太小(-Xmx配置不足)
- 也可能因为老年代已被泄漏对象占满,GC后仍无法满足一次中等大小的对象分配
- 还可能是元空间、栈、直接内存等其他区域耗尽,但堆结构图最直观反映的是“堆空间申请失败”这一结果
结构图上看:新生代频繁GC、老年代水位持续逼近100%、GC日志显示“After GC, old gen used = 99.8%”,下一秒new一个对象就崩——这就是溢出的临界态。
一张图看清因果关系
堆内存结构图不是静态快照,而是动态压力仪表盘:
- 泄漏是慢性病:老年代曲线缓慢爬升,GC频率逐渐增加,但每次回收量越来越少(因为很多对象“可达却不该活”)
- 溢出是急性发作:某次分配请求触发GC → Full GC后老年代仍超阈值 → JVM拒绝分配 → 抛OOM
- 泄漏常是溢出的前奏,但溢出不一定是泄漏引起(比如一次性加载500MB JSON到内存,堆够大也不会泄漏,但瞬间就OOM)
怎么快速判断是泄漏还是溢出?
看现象比看代码更快:
- 应用运行几小时/几天后才OOM,且堆内存监控呈稳定上升趋势 → 高概率泄漏
- 刚启动就OOM,或某次批量操作(导出报表、上传大文件)立即OOM → 更可能是溢出,未必泄漏
- jstat -gc PID 显示FGC次数多、老年代使用率居高不下、每次FGC后回收极少 → 典型泄漏信号
- 堆dump打开后,MAT分析显示大量对象被static字段、ThreadLocal、监听器等意外持有 → 锁定泄漏根因










