你有没有遇到过这样的情况:程序跑着跑着,内存占用越来越高,最后直接卡死或者崩溃。特别是Java应用,任务没多大事儿,堆内存却一路飙升,监控图表像坐了火箭。这时候别急着重启服务,先看看是不是代码里藏着“内存泄漏”的坑。
什么是堆内存?
简单说,堆内存就是Java用来存放对象的地方。new出来的对象基本都在这儿。JVM会自动回收不用的对象,但前提是它得知道哪些对象真的没人用了。如果某些对象本该被回收,却因为被意外引用而一直留着,那内存自然就越积越多。
常见“内存增长”陷阱
最常见的场景之一是静态集合类滥用。比如用一个static的List存数据,想着方便共享,结果往里面塞了一堆临时对象,却不做清理。
public class CacheStore {
private static List<Object> cache = new ArrayList<>();
public static void addData(Object obj) {
cache.add(obj); // 数据只进不出
}
}
这个cache永远不会被清空,每调一次addData,堆就胖一点。时间一长,OOM(OutOfMemoryError)就在路上了。
监听器和回调函数也容易出事
在GUI或事件驱动的应用中,注册了监听器却忘了注销,对象就会一直被持有。比如Swing里给按钮加了个监听,页面关了但监听还在,对应的窗口对象没法被回收。
缓存没设上限
自己手写缓存时,很多人直接用HashMap存东西,觉得方便。但没有过期机制、没有大小限制,缓存越堆越多,最终拖垮内存。
更好的做法是使用带淘汰策略的缓存工具,比如Guava Cache:
Cache<String, Object> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
这样既能保留常用数据,又不会无限制膨胀。
线程池也可能背锅
定义一个无限增长的线程池,比如用Executors.newCachedThreadPool(),短时间内提交大量任务,会创建巨多线程,每个线程都有自己的栈,也会间接推高内存使用。更稳妥的方式是用FixedThreadPool,并设置合理的队列容量。
怎么查问题?
光猜不行,得看证据。可以用jmap生成堆转储文件,再用VisualVM或Eclipse MAT打开,看看哪些类实例最多,占内存最大。通常一眼就能发现异常的集合或缓存对象。
运行中的程序也可以用jstat观察GC情况:
jstat -gc <pid> 1000
如果发现老年代使用率持续上升,Full GC后也没回落,基本可以断定有内存泄漏。
第三方库也要小心
有时候问题不在自己的代码,而是引入的库有问题。比如某个版本的HttpClient没正确关闭连接,或者日志框架异步队列堆积。升级到修复版本,或者检查使用方式是否合规,往往能解决问题。
别忽略字符串常量池
频繁使用String.intern(),尤其是在处理大量不同字符串时,会让永久代(或元空间)压力变大。虽然不算堆内存,但整体内存增长的表现类似,排查时别漏掉这块。