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 知识体系总览

下面这张图,是我自己总结的内存泄漏知识框架。你可以把它当作本章的「地图」:

内存泄漏 什么是内存泄漏 vs 内存溢出 常见分类 常发性 偶发性 一次性 隐式 最终后果:内存溢出(OOM) 应用崩溃 / 服务不可用

这张图把本章的核心内容串起来了。从定义到分类,再到最终后果,一目了然。

1.5 小结

好了,第一章就聊这么多。总结几个关键点:

  • 内存泄漏:对象用不到,但GC收不走。
  • 内存溢出:堆满了,分配不了新对象。泄漏是原因之一,但不是唯一原因。
  • 四种分类:常发性、偶发性、一次性、隐式。每种都有不同的排查思路。

我个人觉得,理解这些基础概念,比直接上手调JVM参数更重要。方向错了,工具再强也没用。

下一章,咱们聊聊如何用工具定位内存泄漏。到时候我会分享一些实战中「一招制敌」的技巧。

一个小建议:看完这一章,你可以打开自己的项目,看看有没有静态集合、ThreadLocal、监听器注册这些「高危地带」。提前扫一遍,能省很多事。

可可精品资料库