C 语言程序员写代码,最怕的就是内存泄漏——malloc 了不 free,跑两天程序就崩了。Java 程序员觉得自己很幸福:有 GC(垃圾回收器),用完自动帮你回收,不需要手动管理内存。但这个”幸福”是假象。Java 也有内存泄漏,而且更难排查。C 语言的内存泄漏是”你忘了释放”,Java 的内存泄漏是”GC 想回收但收不掉”——因为有东西还在引用着它。今天用 7 段代码,把这 7 种最常见的 Java 内存泄漏全拆了。每一种都在生产环境里出过事。
一、静态集合类——最大的”内存黑洞”
静态集合是 Java 内存泄漏的第一大杀手。你往 static Map 里塞数据,觉得是”缓存”,其实是一个无限膨胀的内存黑洞。https://wxa.wxs.qq.com/tmpl/px/base_tmpl.html
二、ThreadLocal 没有 remove
ThreadLocal 在 Spring 框架里无处不在——SecurityContext、RequestContext、TransactionContext 都用了它。如果你自己写代码用了 ThreadLocal,记住:set 了就一定要 remove。
三、监听器和回调没有注销
这个坑在 Android 开发里尤其严重。注册了 BroadcastReceiver 没注销,整个 Activity 都回收不了。Java 后端也一样——手动注册的 Spring Listener 没注销,对象永远挂在内存里。
四、未关闭的资源——IO 流、连接、锁
“Too many open files”这个报错你见过吗?如果见过,大概率就是资源没关。Java 的 try-with-resources 解决了 90% 的问题,但剩下 10% 是那些没有实现 AutoCloseable 的资源——比如 JNI 调用的 native 内存。
五、内部类持有外部类引用
非静态内部类持有外部类引用——这是一个非常隐蔽的泄漏。代码看起来没问题,GC 也找不到”谁在引用”,因为引用链是隐式的。
六、过期对象——缓存了但不知道过期
缓存是好事,但缓存没有过期时间就是内存泄漏。C 语言程序员不会遇到这个问题——因为他们根本不会用 HashMap 做缓存,他们会用 Redis。
七、正则表达式——隐藏的内存炸弹
正则表达式本身不是内存泄漏,但 Pattern.compile 重复编译 + 回溯灾害,是生产环境中很容易被忽视的性能和内存问题。一句话记住:Just because the garbage collector exists doesn’t mean you can stop thinking about memory.https://wxa.wxs.qq.com/tmpl/px/base_tmpl.html(垃圾回收器存在,不代表你可以不再思考内存问题。)
总结:Java 内存泄漏速查表
| 类型 | 根因 | 修复方式 |
|---|---|---|
| 静态集合 | 生命周期 = 应用生命周期 | 用有界缓存(Caffeine) |
| ThreadLocal | 线程池复用,没 remove | finally 里 remove() |
| 监听器/回调 | 注册了没注销 | register/unregister 配对 |
| 未关闭资源 | IO 流、连接没关 | try-with-resources |
| 内部类引用 | 非静态内部类持有外部类引用 | 用 static 内部类或 lambda |
| 过期对象 | 缓存没设过期时间 | 用 Caffeine/Guava Cache |
| 正则重复编译 | 每次调用都 compile | 预编译为 static final |
一句话总结:Java 有 GC,但 GC 不是万能的。有引用的对象,GC 收不掉。
大家有任何不懂的地方,或者项目搭建遇到了问题,都可以在评论区留言,我都会一一回复。如果这篇内容对你有帮助,别忘了点赞、在看、转发给身边同样在学Java的朋友。
我们下期见。