6、内存泄漏:什么是内存泄漏、常见场景、检测工具

内存泄漏,说白了就是「借了东西不还」。

你向系统申请了一块内存,用完之后忘了释放。这块内存就一直被占用着,谁也动不了。程序跑得越久,泄漏越多,最后系统内存被吃光,程序崩溃。

我刚开始写C语言时,觉得内存泄漏离自己很远。直到有一次,我写了一个后台服务,跑了三天三夜,突然挂了。查了半天,发现是一个循环里每次malloc都没free。嗯,从那以后,我对内存管理再也不敢马虎了。

6.1 什么是内存泄漏

内存泄漏(Memory Leak)是指:程序动态分配的内存,在不再需要时没有被释放。这些内存虽然不再被程序使用,但操作系统认为它们仍被占用,无法回收给其他程序使用。

你想想看,这就像你租了一间仓库,东西搬走了,但钥匙没还。仓库管理员以为你还用着,就一直给你留着。时间一长,仓库全被你占满了,别人想租也租不到。

核心要点:内存泄漏的本质是「丢失了指向堆内存的指针」。指针丢了,那块内存就再也找不回来了。

6.2 常见场景

我在项目中遇到过不少内存泄漏的案例。总结下来,最常见的就这几种:

场景一:malloc后忘记free

这是最基础、也最常见的错误。分配了内存,用完了直接跑路。

void func() {
    int *p = (int*)malloc(100 * sizeof(int));
    // 用p做了一些操作...
    // 忘记free(p);
    return;  // 内存泄漏!
}

每次调用func,就泄漏400字节(假设int占4字节)。调用1000次,泄漏400KB。看起来不多?但如果是服务器程序,一天调用几百万次呢?

场景二:指针被覆盖

这个坑我踩过。你分配了一块内存,保存了地址。然后不小心把指针指向了别处,原来的地址就丢了。

int *p = (int*)malloc(100 * sizeof(int));
p = (int*)malloc(200 * sizeof(int));  // 第一次分配的地址丢了!
// 第一次分配的内存永远无法释放了

为什么会这样?因为第二次malloc覆盖了p的值。第一次分配的那块内存,没人知道它在哪了。

场景三:函数返回时未释放

有些函数在异常分支里忘了释放内存。这种问题特别隐蔽。

char* read_file(const char* path) {
    char *buffer = (char*)malloc(1024);
    FILE *fp = fopen(path, "r");
    if (fp == NULL) {
        return NULL;  // 内存泄漏!buffer没释放
    }
    // 正常读取文件...
    fclose(fp);
    return buffer;
}

调用者以为返回NULL就没问题了。实际上,buffer已经泄漏了。

场景四:容器中的内存泄漏

链表、树、哈希表这些数据结构,删除节点时只删了节点本身,忘了释放节点里存储的数据。

struct Node {
    int *data;
    struct Node *next;
};

void delete_node(struct Node *prev) {
    struct Node *target = prev->next;
    prev->next = target->next;
    free(target);       // 只释放了节点
    // target->data 没释放!泄漏了
}

6.3 检测工具

说实话,靠肉眼找内存泄漏,效率太低了。尤其项目大了以后,代码成千上万行,你根本看不出来。我建议用工具。

工具 平台 特点
Valgrind Linux 最常用,功能强大,检测准确
AddressSanitizer Linux/macOS 编译时插桩,速度快,适合大型项目
Dr.Memory Windows/Linux 跨平台,使用简单
Visual Studio 诊断工具 Windows 集成在IDE中,可视化方便

Valgrind 使用示例

我个人习惯用Valgrind。用法很简单:

// 编译时加 -g 保留调试信息
gcc -g -o myapp myapp.c

// 用valgrind运行
valgrind --leak-check=full ./myapp

输出结果会告诉你:哪行代码分配的内存没释放,泄漏了多少字节。清清楚楚。

==12345== 400 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2B800: malloc (vg_replace_malloc.c:299)
==12345==    by 0x4005E4: func (test.c:5)
==12345==    by 0x4005F8: main (test.c:12)

看到没?它直接告诉你test.c第5行malloc的400字节泄漏了。省心省力。

AddressSanitizer 使用示例

如果你用Clang或GCC 4.8以上版本,可以试试AddressSanitizer。编译时加个参数就行:

gcc -fsanitize=address -g -o myapp myapp.c
./myapp

程序运行结束时,如果有内存泄漏,它会自动打印报告。而且它比Valgrind快很多,适合跑大型测试。

我的建议:开发阶段用Valgrind做全面检查,CI/CD流水线里用AddressSanitizer做自动化检测。两个搭配着用,基本能覆盖大部分内存问题。

6.4 内存泄漏的后果

你可能觉得泄漏一点内存没什么。我告诉你,后果很严重:

  • 程序变慢:内存不足时,系统开始用交换分区(swap),速度慢得像蜗牛
  • 系统崩溃:内存耗尽,操作系统会杀掉你的进程(OOM Killer)
  • 服务不可用:服务器程序泄漏内存,跑几天就得重启一次
  • 难以排查:泄漏往往不是立即显现的,可能运行几小时甚至几天后才出问题

我曾经维护过一个数据采集程序,每半小时泄漏几百KB。刚开始没在意,结果跑了两个月后,内存占用从50MB涨到了2GB。最后系统直接OOM,把进程杀了。数据丢了整整一天。

避坑指南:写代码时养成好习惯——malloc和free成对出现。每写一个malloc,立刻写对应的free。别想着「等会儿再补」,等会儿你就忘了。

6.5 知识体系图

下面这张图,帮你理清内存泄漏的核心脉络:

内存泄漏知识体系 内存泄漏 动态分配的内存未被释放 常见场景 检测工具 严重后果 忘记free 指针被覆盖 异常分支 Valgrind AddressSanitizer 程序变慢 系统崩溃

6.6 预防内存泄漏的好习惯

最后,分享几个我自己的经验:

  1. 谁分配,谁释放:哪个函数malloc的,就由哪个函数free。别跨函数释放,容易乱
  2. 初始化指针为NULL:free之后立刻把指针置NULL,防止野指针
  3. 写代码时就把free写好:别想着「回头再补」,回头你就忘了
  4. 定期用工具检查:每周跑一次Valgrind,把泄漏扼杀在摇篮里
  5. 代码审查时重点关注:Review代码时,专门检查malloc/free是否成对
一个小技巧:我习惯在malloc后面空一行,先写free,再写中间的代码。这样就不会忘了。虽然看起来有点怪,但确实管用。

公众号:蓝海资料掘金营,微信deep3321