Java 不是有 GC 吗,为什么还会内存泄漏?

没错,Java 拥有垃圾回收机制,但它并不会替你收拾所有“烂摊子”。只要你无意中留下一个未被断开的引用,GC 就不会回收那个对象。这时虽然不会报错、不会崩溃,但内存却在悄悄上涨,最终被你自己“撑爆”。

这就是我们所说的 Java 内存泄漏


这些“低级错误”,到 2025 年我们还在犯

有意思的是,很多 Java 内存泄漏并不复杂,反而特别“直白”,但我们依然频频踩坑。

1. 静态变量 + 缓存 = 内存被永久占用

你可能只是想临时存点数据,就随手写了个 static Map

java

private static final Map<String, Object> cache = new HashMap<>();

然后这些缓存对象就永远留在内存中,即使不再使用,GC 也束手无策。

2. 监听器注册后忘记注销

特别是在使用事件总线、观察者模式或定时任务时,如果注册了监听器却未调用 removeListener(),相关对象就会一直被引用。这类“忘记解绑”的问题,在老旧系统或长期运行的服务中极为常见。

3. 自定义 ClassLoader 导致“类堆积”

如果你使用热加载插件,或运行像 Tomcat 这类支持类重载的容器,稍不注意就可能泄漏多个 ClassLoader。每个类加载器都携带一批 class 实例,时间一长,JVM 内存中就像堆积了数十个重复的代码版本。

4. ThreadLocal 使用后未清理

ThreadLocal 确实方便,但如果你在线程池环境中使用后未调用 .remove(),那么对应的值就会一直绑定在线程上。只要线程不销毁,这些对象就无法被回收。


工具不是万能的

有人可能会说:“我用了 VisualVM、YourKit 或 MAT,没报错啊。”

但 Java 的内存泄漏不同于 C/C++ 那种“立即崩溃”,它更像是一种慢性病——慢慢拖慢你的应用,逐渐吃光内存,直到最终 OOM(内存溢出)。

等到你意识到问题时,CPU 可能已经爆满,而用户早在社交媒体上抱怨你的服务“像在用脚发电”。


Spring Boot 就安全吗?别太天真

Spring Boot 确实强大好用,但它也拦不住你亲手制造内存泄漏:

  • 单例 Bean 永不销毁;

  • 缓存只写不清;

  • Controller 接收大文件后未释放引用;

  • 使用 ThreadLocal 却忘记清理。

所以别过度迷信框架,它并不是保险箱,反而可能让你更快地“批量生产”问题。


如何避免:让 GC 真正发挥作用

记住一个核心原则:GC 只回收“不可达”的对象。只要你仍持有引用,它就不会被清理。

✅ 明确管理对象引用

及时释放不再使用的对象,避免无意义的强引用滞留。

✅ 合理使用弱引用(WeakReference / SoftReference)

对于缓存、监听器等场景,若非必要不应使用强引用,优先考虑弱引用或软引用。

✅ ThreadLocal 必须手动清理

尤其是在线程池环境下,务必在使用后调用 .remove(),否则就是“内存跟着线程跑”。

✅ 本地复现 + 实用工具推荐:ServBay

线上排查内存泄漏风险高、成本大,建议尽量在本地复现。推荐使用 ServBay——一个支持多语言、多版本的本地开发环境平台,非常适合模拟“灾难场景”。

你可以借助它:

  • 快速切换不同版本 JDK;

  • 一键启动独立 Java 服务,隔离主机环境;

  • 结合 GC 日志、jmap、jstack 等工具抓取堆快照进行分析;

  • 测试不同垃圾回收策略对内存行为的影响。

简单说,开发环境就该让问题暴露得明明白白,而 ServBay 正是这样一个“安心试错”的利器。


最后一句实话

Java 的 GC 确实帮你省了很多事,但它绝不是“万无一失”的。

代码写得不规范、引用没及时断开,该泄漏的照样泄漏。而且正因为“不报错”,这类问题常常拖到深夜才爆发——那才是真正的“程序员噩梦”。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐