1. 内存泄漏基础:什么是内存泄漏、内存泄漏的危害、常见的内存泄漏类型

大家好,我是你们这堂课的老朋友。今天咱们来聊聊内存泄漏——这个让无数程序员头疼的问题。

说实话,我入行头三年,踩过最大的坑就是内存泄漏。有一次线上服务跑了两周,内存从 2GB 涨到 12GB,最后 OOM 被系统杀掉。大半夜被叫起来排查,那滋味,啧,真不好受。

所以这一章,我想把内存泄漏的底裤扒干净。咱们从最基础的概念讲起,一步步深入。

1.1 什么是内存泄漏?

内存泄漏,说白了就是:程序申请了一块内存,用完之后没还给系统。

你想想看,你找图书馆借了本书,看完不还。图书馆的书架就少了一本。时间长了,别人想借也借不到。程序也一样——内存越占越多,系统越来越卡,最后崩了。

用代码来感受一下:

// C++ 示例:典型的内存泄漏
void leak_example() {
    int* p = new int(42);  // 申请内存
    // 忘记 delete p;
    // 函数结束,指针 p 销毁,但内存没释放
}

这段代码里,new 出来的 int 对象,永远留在了堆上。没人能再访问它,也没人释放它。这就是泄漏。

Python 里也有类似情况:

# Python 示例:循环引用导致泄漏
class Node:
    def __init__(self):
        self.ref = None

a = Node()
b = Node()
a.ref = b
b.ref = a  # 互相引用,GC 无法回收
# 即使 del a, del b,内存依然不释放

嗯,这里要注意:Python 有垃圾回收(GC),但循环引用这种场景,GC 不一定能及时处理。我遇到过好几次,就是因为这个,服务内存越跑越高。

1.2 内存泄漏的危害

你可能觉得:「泄漏一点内存而已,多大点事?」

我告诉你,真不是小事。我归纳了四个层面的危害:

危害等级 具体表现 实际影响
轻度 内存缓慢增长 服务运行几天后变慢,重启后恢复
中度 频繁 GC / 页面交换 响应延迟飙升,CPU 占用异常
重度 OOM(内存耗尽) 进程被系统杀死,服务中断
致命 系统级内存耗尽 整台机器卡死,影响其他服务

我个人习惯把内存泄漏比作「慢性病」。它不像空指针那样立刻崩溃,而是慢慢蚕食你的系统资源。等你发现的时候,往往已经晚了。

⚠️ 警告: 线上环境的内存泄漏,最可怕的是「累积效应」。每次请求泄漏 1KB,看起来不多。但一天 1000 万请求呢?那就是 10GB。三天就 30GB。再大的机器也扛不住。

1.3 常见的内存泄漏类型

根据我这些年的经验,内存泄漏大致分这么几类。咱们一个一个看:

1.3.1 堆内存泄漏

这是最经典的类型。C/C++ 里 malloc 了没 free,new 了没 delete。Python 里对象引用计数没归零。

// C 语言:堆泄漏
void heap_leak() {
    char* buf = (char*)malloc(1024);
    // 用完了,但没 free(buf)
    // 每次调用泄漏 1KB
}

我曾经在一个嵌入式项目里,发现一个函数每次调用泄漏 256 字节。那个函数每秒被调用 1000 次。你算算,一小时泄漏多少?接近 900MB。嵌入式设备总共才 512MB 内存,半小时就挂了。

1.3.2 资源泄漏

不只是内存会泄漏。文件句柄、网络连接、数据库连接、图形上下文……这些系统资源,用完了不释放,也是泄漏。

// Java 示例:文件句柄泄漏
public void readFile() throws IOException {
    FileInputStream fis = new FileInputStream("test.txt");
    // 读取操作...
    // 忘记调用 fis.close()
    // 文件句柄一直占着
}

为什么我把这个算作内存泄漏?因为这些资源对象本身也占内存。而且系统对句柄数量有限制,泄漏多了,新的资源申请会失败。

1.3.3 逻辑泄漏

这种最隐蔽。代码没有语法错误,也没有明显的忘记释放。但业务逻辑导致对象一直存活。

举个例子:

// 全局缓存,只增不减
static Map<String, Session> sessionCache = new HashMap<>();

public void onLogin(String userId) {
    Session s = new Session(userId);
    sessionCache.put(userId, s);  // 永远不删除
}

用户登录一次,就往缓存里加一个 Session。用户退出登录呢?没处理。时间长了,缓存里全是僵尸 Session。

💡 我的建议: 设计缓存时,一定要考虑淘汰策略。LRU、TTL、最大容量限制,至少选一个。别让缓存变成「内存黑洞」。

1.3.4 回调/监听器泄漏

注册了事件监听器,但忘记注销。对象本该被回收,但因为监听器持有引用,GC 无法处理。

// Android 示例:匿名内部类持有外部引用
public class MyActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 匿名内部类隐式持有 MyActivity 的引用
        findViewById(R.id.button).setOnClickListener(v -> {
            // 处理点击
        });
    }
    // Activity 销毁时,如果监听器没注销,Activity 无法被回收
}

我记得有一次排查 Android 应用的内存泄漏,发现一个 Activity 被创建了 50 次,但只销毁了 3 次。剩下的 47 个全被监听器「绑架」了。用户切个屏,内存就涨一截。

1.3.5 静态集合泄漏

把对象放到 static 的 List 或 Map 里。静态变量的生命周期和进程一样长。放进去的对象,永远别想被回收。

// 经典反模式
public class GlobalData {
    public static List<byte[]> allData = new ArrayList<>();
    
    public static void addData(byte[] data) {
        allData.add(data);  // 越加越多,永不释放
    }
}

这种代码我见过太多次了。新手图方便,把数据往静态集合里一塞。结果呢?内存泄漏到天荒地老。

1.4 本章小结

好了,咱们把内存泄漏的底细摸清楚了:

  • 什么是泄漏:申请了没释放,内存有去无回
  • 危害有多大:从变慢到崩溃,从进程到整机
  • 常见类型:堆泄漏、资源泄漏、逻辑泄漏、回调泄漏、静态集合泄漏

我个人觉得,理解这些基础概念,比学会用工具更重要。因为工具只能告诉你「哪里泄漏了」,但只有你理解了原理,才能知道「为什么泄漏」和「怎么避免」。

下一章,咱们会深入讲内存分配和释放的底层机制。到时候我会用一张自己画的流程图,把整个过程串起来。嗯,先卖个关子。

核心一句话: 内存泄漏的本质,就是「该回收的没回收」。记住这句话,后面所有内容都围绕它展开。

内存泄漏知识体系 内存泄漏 定义:申请未释放 危害:慢性崩溃 5种常见类型 ① 堆内存泄漏 ② 资源泄漏 ③ 逻辑泄漏 ④ 回调泄漏 / ⑤ 静态集合泄漏 轻度→中度→重度→致命 累积效应:1KB × 1000万 = 10GB C++: new/delete 不配对 Python: 循环引用
📌 小贴士: 上面这张图是我自己画的,把内存泄漏的知识体系串起来了。你可以把它当作「思维导图」来用。每次排查泄漏时,对照这张图,看看属于哪一类,对症下药。

可可精品资料库