内存泄漏排查实战

📅 2026/7/22 19:00:56 👁️ 阅读次数 📝 编程学习
内存泄漏排查实战

内存泄漏排查实战

一、内存泄漏概述

内存泄漏是指程序中已分配的内存空间在不再使用时未能正确释放,导致系统可用内存逐渐减少。

常见泄漏场景

  1. 静态集合持有对象引用
  2. 监听器未正确注销
  3. 缓存未设置过期策略
  4. 线程池中的ThreadLocal未清理
  5. 资源流未关闭

二、检测工具

2.1 jstat

# 查看堆内存使用情况 jstat -gc <pid> 1000 10 # 输出示例 S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT 1024.0 1024.0 0.0 512.0 8192.0 4096.0 16384.0 8192.0 2048.0 512.0 5 0.200 2 0.500 0.700

2.2 jmap

# 生成堆转储文件 jmap -dump:format=b,file=heap.hprof <pid> # 查看堆内存统计 jmap -histo <pid> | head -20

2.3 jhat

# 分析堆转储文件 jhat -port 7000 heap.hprof # 访问 http://localhost:7000 查看分析结果

2.4 VisualVM

VisualVM提供可视化界面:

  • 内存使用趋势图
  • GC活动监控
  • 堆转储分析
  • 内存泄漏检测

三、排查流程

3.1 确认泄漏

# 观察GC行为 jstat -gcutil <pid> 5000 # 如果FGC频繁且OU持续增长,可能存在泄漏

3.2 定位泄漏对象

# 生成堆转储 jmap -dump:live,format=b,file=heap.hprof <pid> # 分析大对象 jhat heap.hprof

3.3 分析引用链

# 使用jhat查询对象引用 # 在浏览器中访问OQL查询: SELECT * FROM java.util.ArrayList WHERE size > 10000

四、常见泄漏模式

4.1 静态集合泄漏

问题代码

public class CacheManager { private static Map<String, Object> cache = new HashMap<>(); public static void addToCache(String key, Object value) { cache.put(key, value); // 永远不会被清理! } }

修复方案

public class CacheManager { // 使用弱引用 private static Map<String, WeakReference<Object>> cache = new WeakHashMap<>(); // 或设置过期策略 private static Cache<String, Object> cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .build(); }

4.2 ThreadLocal泄漏

问题代码

public class UserContext { private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>(); public static void setUser(User user) { userThreadLocal.set(user); } // 缺少remove()调用 }

修复方案

public class UserContext { private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>(); public static void setUser(User user) { userThreadLocal.set(user); } public static void clear() { userThreadLocal.remove(); // 必须清理 } } // 使用try-finally确保清理 try { UserContext.setUser(user); // 业务逻辑 } finally { UserContext.clear(); }

4.3 监听器泄漏

问题代码

public class EventBus { private List<EventListener> listeners = new ArrayList<>(); public void register(EventListener listener) { listeners.add(listener); } // 缺少unregister方法 }

修复方案

public class EventBus { // 使用弱引用列表 private List<WeakReference<EventListener>> listeners = new ArrayList<>(); public void register(EventListener listener) { listeners.add(new WeakReference<>(listener)); } // 定期清理失效引用 public void cleanUp() { listeners.removeIf(ref -> ref.get() == null); } }

4.4 资源流泄漏

问题代码

public void readFile(String path) throws IOException { FileInputStream fis = new FileInputStream(path); // ... 业务逻辑 // 缺少close() }

修复方案

// 使用try-with-resources public void readFile(String path) throws IOException { try (FileInputStream fis = new FileInputStream(path)) { // ... 业务逻辑 } // 自动关闭 }

五、高级排查技巧

5.1 使用Arthas

# 启动Arthas java -jar arthas-boot.jar # 查看堆内存 heapdump --live heap.hprof # 查看类实例数量 sc -c <classloader> -d com.example.* # 监控方法调用 trace com.example.Service process

5.2 OQL查询

// 查询大字符串 SELECT s FROM java.lang.String s WHERE s.value.length > 10000 // 查询被多个对象引用的实例 SELECT o FROM java.lang.Object o WHERE referrers(o).size > 10

5.3 内存泄漏检测工具

工具功能适用场景
Eclipse MAT堆转储分析深度分析泄漏根源
YourKit性能分析实时内存监控
JProfiler综合分析企业级应用

六、预防措施

6.1 代码审查清单

  • 静态字段是否持有不必要的引用
  • ThreadLocal是否在finally块中清理
  • 资源流是否使用try-with-resources
  • 监听器是否有注销机制
  • 缓存是否设置过期策略

6.2 监控告警

// 内存使用监控 public class MemoryMonitor { private static final double THRESHOLD = 0.8; public static void checkMemory() { Runtime runtime = Runtime.getRuntime(); double usage = (double) runtime.totalMemory() / runtime.maxMemory(); if (usage > THRESHOLD) { // 发送告警 alertService.send("内存使用率超过80%"); } } }

七、总结

内存泄漏排查需要系统性方法:通过监控工具发现异常,生成堆转储分析对象引用链,定位泄漏根源。结合代码审查和预防性措施,能够有效避免内存泄漏问题。