1. 内存泄漏基础概念
大家好,我是你们的老朋友。今天咱们聊聊内存泄漏——这个让无数Java开发者头疼的问题。
先问个问题:你写的Java程序,是不是跑着跑着就变慢了?甚至直接OOM挂掉?
嗯,我猜你遇到过。我本人就踩过不少坑。有一次线上服务半夜挂了,查了半天,发现是一个HashMap无限膨胀……那滋味,真不好受。
所以,第一章咱们先把基础打牢。搞懂什么是内存泄漏,它跟内存溢出有啥区别,以及常见的分类有哪些。
1.1 什么是内存泄漏
说白了,内存泄漏就是:对象不再被程序使用,但垃圾回收器(GC)却无法回收它。
你想想看,Java有自动垃圾回收,按理说不用我们操心。但现实是,有些对象虽然程序用不到了,却仍然被某些引用链「牵着」,GC一看:哦,还有人引用你,那我不动你。
结果呢?这些对象就赖在堆里不走。日积月累,堆内存越来越满。
核心定义:内存泄漏 = 无用对象 + 无法回收
我在项目中遇到过最典型的例子:一个全局的静态集合,不断往里加数据,但从不清理。程序跑了三天,集合里塞了几百万个对象,GC都累趴了。
1.2 内存泄漏 vs 内存溢出
这两个概念经常被混用。我习惯这么区分:
| 概念 | 本质 | 关系 |
|---|---|---|
| 内存泄漏 | 对象无法被回收,占用内存 | 泄漏是原因 |
| 内存溢出 | 堆内存耗尽,无法分配新对象 | 溢出是结果 |
举个例子你就明白了:
- 内存泄漏:你家的水龙头没关紧,一直在滴水。
- 内存溢出:水盆满了,水溢出来了。
泄漏是慢慢发生的,溢出是最终的结果。但注意:不是所有溢出都是泄漏引起的。比如你一次性加载了一个超大的文件,内存直接爆了,这叫「瞬时大对象分配」,跟泄漏没关系。
避坑指南:我曾经遇到一个案例,服务OOM了,团队第一反应就是查泄漏。结果查了两天,发现是某个接口返回的数据量突然暴增了100倍。所以,OOM不一定是泄漏,也可能是流量突增或配置不合理。
1.3 内存泄漏的常见分类
根据发生频率和特点,我把内存泄漏分成四类。这四种我都亲手处理过,一个一个说。
1.3.1 常发性内存泄漏
这种最「老实」。每次执行某段代码,都会泄漏固定数量的内存。
比如:
public class LeakyClass {
private static List<byte[]> cache = new ArrayList<>();
public void process() {
// 每次调用都往静态列表里塞1MB数据
cache.add(new byte[1024 * 1024]);
}
}
每次调用process(),就泄漏1MB。跑1000次,就是1GB。这种泄漏最好定位,因为复现路径清晰。
1.3.2 偶发性内存泄漏
这种就有点「狡猾」了。只有在特定条件下才会发生。
我印象很深的一个案例:某个缓存清理逻辑,在并发量低的时候没问题。一旦QPS超过5000,清理线程就跟不上,导致缓存无限膨胀。这就是典型的偶发性泄漏——条件触发。
排查这种泄漏,最头疼的是复现。你可能要压测几个小时才能触发一次。
1.3.3 一次性内存泄漏
这种泄漏只发生一次,但后果可能很严重。
举个例子:应用启动时,加载一个配置文件,解析后生成一个巨大的对象图。但解析器内部有个bug,导致这个对象图无法被回收。虽然只泄漏一次,但如果这个对象占了几百MB,那对应用的影响是致命的。
我的经验:一次性泄漏往往出现在初始化阶段。我建议在启动后做一次堆转储(heap dump),对比启动前后的内存快照,很容易发现这种「一次性」的异常对象。
1.3.4 隐式内存泄漏
这是最「阴险」的一种。代码看起来完全正确,但内存就是悄悄在涨。
常见的场景:
- ThreadLocal使用不当:线程池中的线程复用,ThreadLocal里的值却不清除。
- 内部类持有外部类引用:匿名内部类或非静态内部类,隐式持有外部类的引用。
- 注册监听器未注销:添加了事件监听,但对象销毁时忘了移除。
我处理过一个隐式泄漏:一个定时任务里用了String.intern(),结果字符串池越来越大,最后撑爆了永久代。代码看起来完全合法,但就是泄漏了。
1.4 知识体系总览
下面这张图,是我自己总结的内存泄漏知识框架。你可以把它当作本章的「地图」:
这张图把本章的核心内容串起来了。从定义到分类,再到最终后果,一目了然。
1.5 小结
好了,第一章就聊这么多。总结几个关键点:
- 内存泄漏:对象用不到,但GC收不走。
- 内存溢出:堆满了,分配不了新对象。泄漏是原因之一,但不是唯一原因。
- 四种分类:常发性、偶发性、一次性、隐式。每种都有不同的排查思路。
我个人觉得,理解这些基础概念,比直接上手调JVM参数更重要。方向错了,工具再强也没用。
下一章,咱们聊聊如何用工具定位内存泄漏。到时候我会分享一些实战中「一招制敌」的技巧。
一个小建议:看完这一章,你可以打开自己的项目,看看有没有静态集合、ThreadLocal、监听器注册这些「高危地带」。提前扫一遍,能省很多事。