内存溢出 vs 内存泄漏(分析/案例)
内存溢出 vs 内存泄漏:两个不同的概念
很多人把 OOM 和内存泄漏混为一谈,但它们有本质区别:
| 维度 | 内存溢出(OutOfMemoryError) | 内存泄漏(Memory Leak) |
|---|---|---|
| 本质 | 对象无法分配到足够内存 | 对象不再使用却无法回收 |
| 原因 | 内存耗尽 | 持有不必要的对象引用 |
| 症状 | 立即抛出 OOM | 逐渐耗尽内存 |
| GC 影响 | GC 后仍无法分配 | GC 回收不了 |
| 堆表现 | 堆接近最大值,GC 后仍满 | 堆持续增长,GC 无法回收 |
内存溢出(OOM)
堆溢出
java
public class HeapOOM {
public static void main(String[] args) {
// 不断分配对象,直到堆耗尽
List<byte[]> list = new ArrayList<>();
int i = 0;
try {
while (true) {
list.add(new byte[1024 * 1024]); // 每次分配 1MB
i++;
}
} catch (OutOfMemoryError e) {
System.out.println("分配了 " + i + " MB 后 OOM");
throw e;
}
}
}java.lang.OutOfMemoryError: Java heap space
at HeapOOM.main(HeapOOM.java:9)元空间溢出
java
public class MetaspaceOOM {
public static void main(String[] args) {
// 不断生成新的类,填满 Metaspace
int i = 0;
try {
while (true) {
// 使用 CGLib 或反射动态生成类
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MetaspaceOOM.class);
enhancer.setCallback((MethodInterceptor) (o, m, p, a) -> m.invoke(o, a));
enhancer.create();
i++;
}
} catch (Error e) {
System.out.println("生成了 " + i + " 个类后 OOM");
throw e;
}
}
}java.lang.OutOfMemoryError: Metaspace直接内存溢出
java
public class DirectMemoryOOM {
public static void main(String[] args) {
List<ByteBuffer> buffers = new ArrayList<>();
int i = 0;
try {
while (true) {
// 直接内存分配
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 256); // 256MB
buffers.add(buffer);
i++;
}
} catch (OutOfMemoryError e) {
System.out.println("分配了 " + i + " 块直接内存后 OOM");
throw e;
}
}
}java.lang.OutOfMemoryError: Direct buffer memoryOOM 的常见原因
| 原因 | 说明 | 解决方案 |
|---|---|---|
| 大量对象分配 | 一次性分配太多对象 | 减少对象数量、增加堆大小 |
| 内存泄漏 | 对象无法回收 | 排查泄漏根源 |
| 类加载过多 | 反射/动态代理生成类太多 | 增大 Metaspace |
| 直接内存未释放 | NIO 使用过多 | 增大直接内存上限 |
| 字符串常量池 | 大量 intern() | 增大 StringTable |
内存泄漏(Memory Leak)
内存泄漏的本质
内存泄漏是对象应该被回收,但由于被其他对象引用而无法回收。GC 的可达性分析无法发现这些对象,因为 GC Roots 仍然可以访问它们。
典型泄漏场景一:静态集合持有引用
java
public class StaticCollectionLeak {
// 静态集合持有对象引用,永不释放
private static final Map<String, Object> CACHE = new HashMap<>();
public static void main(String[] args) {
// 不断向静态集合添加数据
// 即使集合中的数据不再需要,也永远不会被回收
// 因为 CACHE 是 GC Root(静态变量)
while (true) {
CACHE.put(UUID.randomUUID().toString(), new byte[1024 * 1024]);
}
}
}GC Roots:静态变量 CACHE
泄漏路径:CACHE(GC Root) → HashMap → Entry → key/value → 对象
典型泄漏场景二:未关闭的资源
java
public class ResourceLeak {
public static void main(String[] args) throws IOException {
// 文件流、数据库连接、网络连接
// 如果不显式关闭,这些对象不会被 GC
// 即使程序不再使用
FileInputStream fis = new FileInputStream("big.txt");
// 没有 fis.close()!
// FileInputStream 持有 native 资源,不关闭 = 内存泄漏
}
}正确做法:
java
// 方式一:try-with-resources(JDK 7+)
try (FileInputStream fis = new FileInputStream("big.txt")) {
// 使用 fis
} // 自动关闭
// 方式二:finally
FileInputStream fis = null;
try {
fis = new FileInputStream("big.txt");
} finally {
if (fis != null) fis.close();
}典型泄漏场景三:监听器未注销
java
public class ListenerLeak {
private static final List<EventListener> LISTENERS = new ArrayList<>();
public void addListener(EventListener listener) {
LISTENERS.add(listener); // 添加了监听器
// 但没有 removeListener() 方法,或调用者忘记移除
}
public static void main(String[] args) {
// 每次创建对象都添加监听器
// 但从未移除
while (true) {
addListener(new EventListener() {
@Override
public void onEvent(Object e) {}
});
}
}
}典型泄漏场景四:ThreadLocal 未清理
java
public class ThreadLocalLeak {
private static final ThreadLocal<byte[]> DATA = new ThreadLocal<>();
public static void main(String[] args) {
// 线程池中的线程会长期存活
// 如果 ThreadLocal 没有清理
// ThreadLocalMap 中的 Entry 就不会被回收
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
pool.submit(() -> {
DATA.set(new byte[1024 * 1024]); // 设置了
// 任务结束后,DATA 没有被 remove()
// 线程复用,DATA 中的数据永远不会被清理
});
}
}
}正确做法:
java
pool.submit(() -> {
try {
DATA.set(new byte[1024 * 1024]);
// 使用 DATA
} finally {
DATA.remove(); // 任务结束后清理
}
});典型泄漏场景五:字符串驻留
java
public class StringInternLeak {
public static void main(String[] args) {
// 大量 intern() 调用,导致 StringTable 持续增长
// JDK 6 中会撑满 PermGen
while (true) {
String s = generateRandomString();
s.intern(); // 不要对随机字符串调用 intern()
}
}
}排查方法
排查 OOM
bash
# 1. 生成堆转储
java -Xmx256m -XX:+HeapDumpOnOutOfMemoryError MyApp
# 2. 生成 dump 后继续运行
java -Xmx256m -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/ \
-XX:+ExitOnOutOfMemoryError \
MyApp排查内存泄漏
bash
# 1. 定期生成堆 dump
jmap -dump:format=b,file=/tmp/heap.hprof <pid>
# 2. 对比多次 dump 的对象数量变化
jmap -histo <pid> | grep <可疑类名>
# 3. 使用 MAT 分析 dump
# MAT(Memory Analyzer Tool)可以:
# - 找出占用最大的对象
# - 追踪引用链路
# - 找出泄漏根源jstat 监控内存趋势
bash
# 每秒输出一次 GC 统计
jstat -gcutil <pid> 1000
# 输出:
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 0.00 65.21 78.33 92.00 88.00 123 2.34 5 12.34 0 14.68
# 如果 O(老年代)持续增长且 FGC(Full GC)后不降 → 内存泄漏内存泄漏 vs 内存溢出 vs 内存不足
内存泄漏:
对象 A 持有对象 B 的引用
A 被 GC Root 引用(A 是有用的)
B 被 A 引用但实际不再使用
B 无法被回收
→ 持续占用内存
内存溢出:
堆 / 元空间 / 直接内存 全部耗尽
无法再分配新对象
→ 抛出 OOM
两者关系:
内存泄漏 → 内存占用持续增长 → 最终内存不足 → OOM本节小结
内存溢出与内存泄漏的对比:
| 维度 | 内存溢出 | 内存泄漏 |
|---|---|---|
| 定义 | 无法分配到足够内存 | 对象无法被 GC 回收 |
| 表现 | 抛出 OutOfMemoryError | 内存持续增长,GC 无法回收 |
| 排查 | dump + MAT 分析 | jstat 监控 + 对比 dump |
| 常见场景 | 堆/元空间/直接内存耗尽 | 静态集合、ThreadLocal、未关闭资源 |
| 解决方案 | 增大内存、优化分配 | 移除不必要的引用 |
理解内存泄漏和溢出的关系,有助于从根本上解决 JVM 内存问题。
到这里,「GC 基础理论」部分全部完成。接下来进入「垃圾回收器(实战)」部分,首先看 GC 性能指标/回收器发展/组合关系。
