智用指南
第二套高阶模板 · 更大气的阅读体验

堆内存不断增长?可能是这些地方出了问题

发布时间:2025-12-11 12:55:08 阅读:292 次

你有没有遇到过这样的情况:程序跑着跑着,内存占用越来越高,最后直接卡死或者崩溃。特别是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(),尤其是在处理大量不同字符串时,会让永久代(或元空间)压力变大。虽然不算堆内存,但整体内存增长的表现类似,排查时别漏掉这块。