A04 —— 内存泄漏:malloc 不 free 是怎么导致泄漏的

金句:程序结束后,系统会帮你回收——但在运行期间,泄漏的内存就是真的泄漏了。


什么是内存泄漏

内存泄漏(Memory Leak)是指程序动态分配内存后,没有释放,导致这块内存永远无法被 reuse。

Bash
内存泄漏的典型例子:

void leak() {
    char *buf = malloc(1024);
    if (some_condition) {
        return;  // 提前返回,没有 free
    }
    free(buf);  // 正常路径释放
}

void real_leak() {
    while (1) {
        char *buf = malloc(1024);
        // 忘记 free(buf);
        sleep(1);
    }
}

Bash
内存泄漏的分类:

1. 真正的泄漏(True Leak):
   - 指针丢失,内存无法访问
   - 内存无法被任何代码释放
   - 必须重启程序才能回收

2. 逻辑泄漏(Logical Leak):
   - 指针还在,但不再使用
   - 内存仍然可达,但已经是垃圾
   - 修复代码可以解决

3. 循环引用(C++ 中):
   - 对象 A 持有对象 B 的引用,B 也持有 A 的引用
   - 两者都无法被 GC 回收
   - 用 weak_ptr 打破循环

泄漏是如何发生的

Bash
常见的内存泄漏场景:

1. 提前 return:
   if (cond) return;  // buf 没有 free
   ...
   free(buf);

2. 错误处理遗漏:
   if (!buf) return ERROR;
   buf = malloc(...);
   if (err) return ERROR;  // buf 没有 free
   ...
   free(buf);

3. 循环中累积:
   while (process()) {
       char *tmp = malloc(1024);
       do_something(tmp);
       // 忘记 free(tmp)
   }

4. 指针赋值覆盖:
   char *buf = malloc(1024);
   buf = malloc(2048);  // 第一个 1024 泄漏
   free(buf);           // 只 free 第二个

进程结束后内存会回收吗

Bash
进程退出后,内存会被系统回收:

- 进程退出时,所有打开的文件描述符关闭
- 所有内存映射(mmap/vma)被释放
- 所有物理页归还 buddy system
- 进程持有的内存全部回收

所以:
  - 泄漏的内存,进程退出后会被回收
  - 但如果进程是长期运行的(服务器),泄漏会累积,直到 OOM

泄漏的影响:
  - 短命程序(脚本):影响小,退出就回收
  - 长命程序(服务器/daemon):累积,最终 OOM Killer 杀掉

检测内存泄漏的工具

Bash
1. valgrind --leak-check=full:

valgrind --leak-check=full ./program

输出:
  ==12345== 120 bytes in 1 blocks are definitely lost
  ==12345==    at 0x...: malloc (vg_malloc.c:...)
  ==12345==    by 0x...: leak() (leak.c:10)

leak summary:
  definitely lost: 120 bytes(真正泄漏)
  indirectly lost: 0 bytes(间接泄漏)
  still reachable: 1024 bytes(全局变量持有,未泄漏)

2. AddressSanitizer:

gcc -fsanitize=address -g program.c
./program

ASan 会在运行时检测:
  - 泄漏(leak)
  - 访问已释放内存(use-after-free)
  - 越界访问(buffer overflow)

3. /proc/PID/status 监控:

$ ps -o pid,rss,vsz,comm | grep myprogram
  PID    RSS    VSZ  COMM
12345  54321 123456 myprogram

如果 RSS 持续增长 = 有泄漏

4. mtrace(glibc 内置):

#include <mcheck.h>
mtrace();

运行:
  MALLOC_TRACE=/tmp/trace.log ./program
  mtrace ./program /tmp/trace.log
  → 报告所有 malloc/free 调用

防止内存泄漏的方法

Bash
C++ 智能指针:

void leak() {
    auto buf = std::make_unique<char[]>(1024);
    // ...
}  // 函数结束,buf 自动释放(delete[])

RAII 模式:

class Buffer {
    char *data;
public:
    Buffer(size_t size) { data = new char[size]; }
    ~Buffer() { delete[] data; }  // 析构自动释放
};

void func() {
    Buffer buf(1024);
    // ...
}  // buf 超出作用域,析构函数自动调用

C 语言 cleanup 属性:

void func() {
    char *buf __attribute__((cleanup(cleanup_free))) = malloc(1024);
    // ...
}

static void cleanup_free(char **p) {
    if (*p) free(*p);
}

谁分配谁释放原则:
  - 每个 malloc 必须配对一次 free
  - 建议封装成函数或用 RAII

总结

  • 内存泄漏:malloc 后未 free,指针丢失,无法回收
  • 真正泄漏:指针丢失,无法访问;逻辑泄漏:指针还在但不用
  • 长命程序:短命程序退出自动回收,长命程序累积导致 OOM
  • 检测:valgrind(最全)/ ASan(快)/ /proc/status(监控)/ mtrace(glibc)
  • 防止:智能指针(C++)/ RAII 析构函数 / cleanup 属性 / 谁分配谁释放

下篇预告(A05):什么是 use-after-free?为什么释放后再访问会导致严重问题,以及怎么检测。


关注公众号「AI不着急」,回复”资料”获取内存学习路线图。

最后修改: 2024年5月27日

作者

评论

发表评论

您的邮箱地址不会被公开。