什么是核心模块内存泄漏
在运行大型系统或长时间服务时,程序占用的内存越来越多,重启后又恢复正常,这很可能是核心模块出现了内存泄漏。尤其是后台服务、数据库引擎、通信中间件这类长期驻留内存的模块,一旦有对象无法被回收,就会像水龙头滴水一样,慢慢耗尽系统资源。
比如某次线上接口响应越来越慢,查监控发现 JVM 堆内存持续上涨,GC 频率越来越高,最终定位到是某个缓存未设置过期时间,导致请求参数不断被存入静态 Map,这就是典型的核心模块内存泄漏问题。
常见泄漏场景识别
静态集合类持有对象是最常见的泄漏源头。例如使用 static HashMap 存储用户会话信息,但没有清理机制,随着时间推移,内存只增不减。
另一个高发区是监听器和回调注册。模块间通过事件通信时,如果注册了监听但未在适当时机反注册,即使对象本该被回收,也会因被事件中心引用而一直存活。
还有线程相关的问题。启动了一个守护线程处理任务,但线程中持有了外部对象的强引用,且线程未正确关闭,那么这些对象也无法释放。
用工具快速定位问题
JVM 环境下,jmap 和 jstat 是基础命令行工具。比如用 jstat -gc pid 1000 可以每秒输出一次 GC 情况,观察老年代是否持续增长。
更直观的是使用 jmap 生成堆转储文件:
jmap -dump:format=b,file=heap.hprof <pid>然后用 Eclipse MAT 或 JVisualVM 打开 hprof 文件,查看哪些类的实例数量异常多,重点关注带有“Cache”、“Manager”、“Pool”字样的类。
代码层如何预防泄漏
合理使用弱引用(WeakReference)或软引用(SoftReference)能有效避免长生命周期容器持有短生命周期对象。例如缓存映射可以用 WeakHashMap,当 key 没有其他引用时,条目会自动被清除。
对于注册机制,提供配套的注销方法,并在 Spring 的 @PreDestroy 或 try-with-resources 中调用。不要让“注册”成为单向操作。
使用线程池代替手动创建线程,避免忘记关闭。同时注意线程局部变量 ThreadLocal 的使用,务必在任务结束时调用 remove(),否则可能引发严重的内存累积。
添加自动化检测机制
在测试环境中模拟长时间运行,结合监控脚本定期抓取内存数据。可以写一个简单的 shell 脚本,每隔一段时间记录一次 RSS 内存值:
while true; do \
echo $(date), $(ps -o rss= -p <pid>) >> mem_usage.log; \
sleep 60; \
done如果发现趋势持续上升,就可以触发告警,提前介入排查。
单元测试中也可以引入第三方库,如 Mockito 配合自定义监听器,验证对象是否被正确释放。或者使用 JUnit 的扩展模型,在测试前后对比堆内存快照,查找未回收实例。