一、什么是内存泄漏?

想象你的手机内存是杯水,每次打开应用都会倒进去一些水。正常情况下,关闭应用时水会倒掉(内存释放)。但如果某些水被“卡”在杯子里倒不出来,时间一长杯子就满了——这就是内存泄漏。在Java中,对象用完后没被垃圾回收(GC)清理,就会导致内存逐渐耗尽,最终程序崩溃。

二、常见的内存泄漏原因

1. 静态集合滥用

静态集合(如HashMap)的生命周期和程序一样长,如果往里塞对象却忘了清理,就会泄漏。

示例(技术栈:Java)

public class StaticLeak {
    private static List<Object> cache = new ArrayList<>(); // 静态集合

    public void addToCache(Object obj) {
        cache.add(obj); // 对象永远无法被GC回收
    }
}
// 问题:即使obj不再使用,也会一直留在cache中

2. 未关闭的资源

数据库连接、文件流等资源如果不手动关闭,会一直占用内存。

示例(技术栈:Java)

public class ResourceLeak {
    public void readFile() {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("data.txt"); // 打开文件流
            // 读取操作...
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 忘记调用 fis.close();
    }
}
// 问题:文件流未关闭,可能导致内存和系统资源泄漏

3. 监听器与回调

注册监听器后忘记注销,会导致监听对象无法被回收。

示例(技术栈:Java)

public class ListenerLeak {
    private static List<EventListener> listeners = new ArrayList<>();

    public void addListener(EventListener listener) {
        listeners.add(listener); // 添加监听器
    }
    // 缺少removeListener方法!
}
// 问题:即使listener对象不再需要,仍被静态集合持有

4. ThreadLocal使用不当

ThreadLocal的变量在线程存活期间会一直存在,线程池中复用线程时容易泄漏。

示例(技术栈:Java)

public class ThreadLocalLeak {
    private static ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();

    public void doTask() {
        threadLocal.set(new byte[1024 * 1024]); // 1MB内存
        // 任务完成后未调用 threadLocal.remove()
    }
}
// 问题:线程池复用线程时,上次的byte[]可能一直未被清理

三、如何排查内存泄漏?

1. 使用工具监控

  • VisualVM:观察堆内存曲线是否持续上升。
  • MAT(Memory Analyzer Tool):分析堆转储文件,找到占用内存最多的对象。

2. 代码审查重点

  • 检查静态集合、全局缓存的使用。
  • 确认所有资源(如IO流、连接)都有try-with-resourcesfinally块关闭。

示例(技术栈:Java)

// 正确的资源关闭方式
try (FileInputStream fis = new FileInputStream("data.txt")) {
    // 自动关闭资源
} catch (IOException e) {
    e.printStackTrace();
}

3. 日志与测试

  • 在关键代码段添加内存日志:Runtime.getRuntime().freeMemory()
  • 压力测试:模拟长时间运行,观察内存是否稳定。

四、预防内存泄漏的最佳实践

  1. 避免静态集合:改用弱引用(WeakHashMap)或定期清理。
  2. 及时释放资源:使用AutoCloseable接口或工具类(如Spring的@PreDestroy)。
  3. 谨慎使用监听器:在对象销毁时主动注销监听。
  4. 清理ThreadLocal:线程任务结束后调用remove()

示例(技术栈:Java)

// 使用WeakHashMap避免泄漏
private static Map<Key, Value> cache = new WeakHashMap<>();
// 当Key不再被其他对象引用时,会自动从Map中移除

五、应用场景与注意事项

应用场景

  • 长期运行的服务(如微服务、定时任务)。
  • 高并发程序(如线程池处理请求)。

技术优缺点

  • 优点:提前预防可大幅提升系统稳定性。
  • 缺点:排查复杂,需结合工具和经验。

注意事项

  • 不要过度依赖GC,显式管理内存更可靠。
  • 第三方库也可能引发泄漏(如某些缓存框架),需关注文档。

总结

内存泄漏像“慢性病”,初期不易察觉,但积累到一定量会致命。通过规范编码、合理使用工具,完全可以避免。记住:没有“偶然”的内存泄漏,只有未被发现的错误代码