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快很多,适合跑大型测试。
6.4 内存泄漏的后果
你可能觉得泄漏一点内存没什么。我告诉你,后果很严重:
- 程序变慢:内存不足时,系统开始用交换分区(swap),速度慢得像蜗牛
- 系统崩溃:内存耗尽,操作系统会杀掉你的进程(OOM Killer)
- 服务不可用:服务器程序泄漏内存,跑几天就得重启一次
- 难以排查:泄漏往往不是立即显现的,可能运行几小时甚至几天后才出问题
我曾经维护过一个数据采集程序,每半小时泄漏几百KB。刚开始没在意,结果跑了两个月后,内存占用从50MB涨到了2GB。最后系统直接OOM,把进程杀了。数据丢了整整一天。
6.5 知识体系图
下面这张图,帮你理清内存泄漏的核心脉络:
6.6 预防内存泄漏的好习惯
最后,分享几个我自己的经验:
- 谁分配,谁释放:哪个函数malloc的,就由哪个函数free。别跨函数释放,容易乱
- 初始化指针为NULL:free之后立刻把指针置NULL,防止野指针
- 写代码时就把free写好:别想着「回头再补」,回头你就忘了
- 定期用工具检查:每周跑一次Valgrind,把泄漏扼杀在摇篮里
- 代码审查时重点关注:Review代码时,专门检查malloc/free是否成对
公众号:蓝海资料掘金营,微信deep3321