内存泄漏排查
问题
服务运行一段时间后内存持续增长不释放,如何定位内存泄漏?
答案
内存泄漏 vs 内存溢出
| 内存泄漏(Memory Leak) | 内存溢出(OOM) | |
|---|---|---|
| 定义 | 对象不再使用但无法被 GC 回收 | 内存不足无法分配新对象 |
| 关系 | 泄漏积累到一定程度 → 溢出 | 可能是泄漏导致,也可能是单次大分配 |
排查流程
堆转储对比分析
多次 dump 对比
# 第一次 dump
jmap -dump:format=b,file=/tmp/heap1.hprof <pid>
# 等待 30 分钟
# 第二次 dump
jmap -dump:format=b,file=/tmp/heap2.hprof <pid>
在 MAT 中:Histogram → 右键 → Compare with another Heap Dump → 找到数量持续增加的类。
六种常见泄漏场景
场景 1:集合只加不删
❌ 泄漏:static 集合无限增长
public class EventManager {
// 只 add 不 remove,随着时间推移 listeners 越来越大
private static final List<EventListener> listeners = new ArrayList<>();
public void register(EventListener listener) {
listeners.add(listener);
}
// 缺少 unregister 方法!
}
✅ 修复
public void unregister(EventListener listener) {
listeners.remove(listener);
}
// 或使用 WeakReference
private static final List<WeakReference<EventListener>> listeners = new ArrayList<>();
场景 2:ThreadLocal 未清理
❌ 线程池 + ThreadLocal = 泄漏
private static final ThreadLocal<UserContext> USER_CTX = new ThreadLocal<>();
public void handleRequest() {
USER_CTX.set(new UserContext(userId));
// 处理业务...
// 线程池复用线程,ThreadLocal 不清理 → 泄漏
}
✅ 修复:finally 中 remove
public void handleRequest() {
try {
USER_CTX.set(new UserContext(userId));
// 处理业务...
} finally {
USER_CTX.remove(); // 必须清理
}
}
场景 3:连接/流未关闭
❌ 连接未关闭
public String query(String sql) {
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// 异常时 conn 不会关闭 → 连接泄漏
String result = rs.getString(1);
conn.close();
return result;
}
✅ 修复:try-with-resources
public String query(String sql) {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
return rs.getString(1);
}
}
场景 4:内部类持有外部类引用
❌ 非静态内部类持有外部类导致泄漏
public class Outer {
private byte[] data = new byte[10 * 1024 * 1024]; // 10MB
// 非静态内部类隐式持有 Outer 引用
public class InnerTask implements Runnable {
@Override
public void run() { /* ... */ }
}
public void start() {
executor.execute(new InnerTask());
// InnerTask → Outer → data,Outer 无法回收
}
}
✅ 修复:使用静态内部类
private static class InnerTask implements Runnable {
@Override
public void run() { /* ... */ }
}
场景 5:缓存无过期策略
详见 OOM 定位 中的本地缓存案例,使用 Caffeine 设置大小限制和过期时间。
场景 6:监听器/回调未注销
❌ 注册后未注销
public class DataProcessor {
public void init() {
// 注册监听器,但 destroy 时没有注销
eventBus.register(this);
}
// 缺少 destroy() { eventBus.unregister(this); }
}
Arthas 在线排查
Arthas 内存相关命令
# 查看堆内存概况
memory
# 查看对象实例数 Top N
dashboard
# 然后按 m 查看内存
# Heap Dump(不需要 jmap)
heapdump /tmp/heap.hprof
# 在线搜索对象实例
vmtool --action getInstances --className com.example.UserContext --limit 10
常见面试问题
Q1: 如何判断是不是内存泄漏?
答案:
- 监控堆内存使用量,如果随时间持续上升不回落(Full GC 后仍然很高),大概率是内存泄漏
- 使用
jstat -gcutil <pid> 1000持续观察 Old 区使用率
Q2: ThreadLocal 为什么会内存泄漏?
答案:
ThreadLocalMap 的 key 是 WeakReference<ThreadLocal>,但 value 是强引用。当 ThreadLocal 被回收后,key 变成 null,但 value 无法回收。线程池场景下线程长期不销毁,导致越积越多。
详见 ThreadLocal。
Q3: 生产环境如何做内存泄漏的预防?
答案:
- ThreadLocal 使用 try-finally 确保 remove
- IO 资源使用 try-with-resources
- 集合类注意 remove / 设置上限
- 内部类优先用 static
- Caffeine / Guava Cache 设置大小和过期
- 监控 + 告警:老年代使用率持续上升告警