内存泄漏检测组件

小捏哩 2026-03-17 09:08:04

内存泄漏检测组件

目录

  • 内存泄漏检测组件
  • 1.什么是内存泄漏?
  • 2.为什么内存泄漏是C/C++特有的问题?
  • 3.为什么会发生内存泄漏?
  • 4.如何判断和排查内存泄漏?
  • 4.1观察现象
  • 4.2工具检测
  • 5.内存泄漏——泄漏的是什么内存?
  • 5.1堆(Heap)vs 栈(Stack)
  • 6.线上出现内存泄漏怎么解决?
  • 6.1热更新检测模块的设计
  • 7.三种内存泄漏检测方案
  • 7.1方案一:数据结构记录法
  • 7.2方案二:文件标记法(宏定义实现)
  • 7.2.1实现代码
  • 7.3方案三:Hook技术(动态链接器拦截)
  • 7.3.1实现代码
  • 8.总结

1.什么是内存泄漏?

内存泄漏(Memory Leak)是指程序在运行过程中动态分配的内存(通过malloc、new等操作)在使用完毕后未能正确释放,导致这部分内存无法被程序后续访问或回收,直到程序结束才由操作系统回收。简单来说,就是申请的内存忘记归还给系统

内存泄漏的严重性在于它是一个累积性错误——即使单次泄漏的内存很小,但随着时间的推移,泄漏的内存会不断累积,最终导致可用内存耗尽,程序性能下降,甚至崩溃。

2.为什么内存泄漏是C/C++特有的问题?

Java、Go、Python等语言都具备垃圾回收(Garbage Collection,GC)机制。GC会定期扫描堆内存,自动回收不再被引用的内存对象,程序员无需手动释放内存。虽然GC也并非万能(可能因引用未断开导致“内存堆积”),但绝大多数情况下,内存管理是自动化的。

而C/C++没有内置的GC机制,内存管理完全交由程序员手动控制:

  • 申请内存:malloc、calloc、realloc、new
  • 释放内存:free、delete

这种设计赋予程序员极大的灵活性,但也带来了责任——任何malloc/new都必须有对应的free/delete,否则就会发生内存泄漏。此外,C/C++中指针的灵活使用(指针运算、指针别名等)也使得内存泄漏问题更加隐蔽和复杂。

3.为什么会发生内存泄漏?

常见的内存泄漏场景包括:

  1. 忘记释放内存——最直接的原因,调用了malloc但没有调用free
  2. 释放不完全——如结构体中包含指向动态内存的指针,只释放了结构体本身,未释放其成员指向的内存
  3. 错误地覆盖了指针——指向动态内存的指针被重新赋值,导致原内存地址丢失,无法释放
  4. 异常分支未释放——函数中有多个return路径,某些路径下忘记释放内存
  5. 容器类使用不当——如用链表存储动态内存,但销毁链表时未遍历释放每个节点的数据
  6. 循环引用——尤其在C++智能指针出现前,两个对象互相持有对方指针,导致引用计数无法归零

4.如何判断和排查内存泄漏?

4.1观察现象

  • 程序运行时间越长,占用内存越大(通过top、htop、任务管理器等观察)
  • 系统响应变慢,甚至触发OOM(Out of Memory) Killer
  • 程序崩溃,尤其是长时间运行的服务

4.2工具检测

  • Valgrind:Linux下强大的内存调试工具,可检测内存泄漏、越界访问等
  • AddressSanitizer(ASan):Google开发的内存错误检测工具,编译时加入-fsanitize=address即可
  • mtrace:glibc自带的内存泄漏检测工具
  • Visual Studio诊断工具:Windows下可使用CRT调试堆函数(_CrtDumpMemoryLeaks)

5.内存泄漏——泄漏的是什么内存?

内存泄漏泄漏的是堆内存(Heap Memory)

5.1堆(Heap)vs 栈(Stack)

特性栈(Stack)堆(Heap)
管理方式编译器自动管理程序员手动管理
分配速度快(仅移动栈指针)慢(需要寻找合适空闲块)
存储内容局部变量、函数参数、返回地址动态分配的对象、数据
生命周期随函数调用结束自动释放需手动释放,或程序结束才回收
内存大小较小(通常几MB)较大(受系统内存限制)
碎片化无(先进后出)易产生外部碎片
典型分配int a = 10;int p = (int)malloc(sizeof(int));

代码归属

  • 栈:函数内部定义的局部变量、函数参数等
  • 堆:通过malloc/calloc/realloc/new分配的内存

6.线上出现内存泄漏怎么解决?

线上环境不能随便停服务,也不能直接挂载Valgrind等重型工具(性能影响太大)。解决思路是让检测模块具备动态开关能力,实现热更新

6.1热更新检测模块的设计

  • 编译时预留检测代码,通过全局变量或配置控制是否开启
  • 动态开关:接收外部信号(如SIGUSR1)或读取配置文件,动态启用/禁用检测
  • 性能损失:开启检测时会有性能开销(文件I/O、锁竞争等),关闭时接近零开销

典型做法:定义一个全局标志g_mem_leak_check_enabled,在malloc/free的包装函数中判断该标志,只有开启时才进行记录操作。

7.三种内存泄漏检测方案

7.1方案一:数据结构记录法

维护一个全局链表或红黑树,每次malloc时插入节点(记录指针地址、大小、调用位置),每次free时删除节点。程序结束时,检查容器中是否还有残留节点,若有则说明发生了泄漏。

优点

  • 实现简单,不依赖外部文件
  • 可实时查询当前未释放的内存块

缺点

  • 需要额外管理锁(多线程环境下)
  • 容器操作本身有性能开销
  • 程序异常退出时记录可能丢失

7.2方案二:文件标记法(宏定义实现)

每次malloc时创建一个临时文件,文件名包含指针地址,文件内容记录分配信息(文件名、函数名、行号、大小)。每次free时删除对应文件。程序结束后,查看剩余文件即可定位泄漏点。

优点

  • 直观,文件系统作为持久化存储,程序崩溃后仍可查看
  • 实现简单,适合单文件或小型项目

缺点

  • 文件I/O开销较大,高频调用下性能影响明显
  • 需要确保./block目录存在且有写权限
  • 多进程/多线程需处理文件名冲突

7.2.1实现代码

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>

// 确保block目录存在
static void ensure_block_dir() {
    struct stat st = {0};
    if (stat("./block", &st) == -1) {
        mkdir("./block", 0755);
    }
}

void *nMalloc(size_t size, const char *filename, const char *funcname, int line) {
    void *ptr = malloc(size);
    if (!ptr) return NULL;

    ensure_block_dir();

    char buff[256];
    snprintf(buff, sizeof(buff), "./block/%p.mem", ptr);

    FILE *fp = fopen(buff, "w");
    if (!fp) {
        free(ptr);
        return NULL;
    }

    fprintf(fp, "[+][%s:%s:%d] %p %zu malloc\n", filename, funcname, line, ptr, size);
    fclose(fp);
    
    return ptr;
}

void nFree(void *ptr, const char *filename, const char *funcname, int line) {
    if (!ptr) return;

    char buff[256];
    snprintf(buff, sizeof(buff), "./block/%p.mem", ptr);

    if (unlink(buff) < 0) {
        // 文件不存在,可能是重复释放或未记录的指针
        printf("Warning: double free or free of unknown pointer: %p at [%s:%s:%d]\n", 
               ptr, filename, funcname, line);
    }

    free(ptr);
}

#define malloc(size) nMalloc(size, __FILE__, __func__, __LINE__)
#define free(ptr) nFree(ptr, __FILE__, __func__, __LINE__)

// 测试代码
int main() {
    int size = 5;
    void *p1 = malloc(size);
    void *p2 = malloc(size * 2);
    void *p3 = malloc(size * 3);

    free(p1);
    free(p3);  // p2 故意不释放,用于演示泄漏
    return 0;
}

7.3方案三:Hook技术(动态链接器拦截)

通过dlsym(RTLD_NEXT, "malloc")获取真实的malloc/free函数地址,然后编写同名包装函数,在其中插入记录逻辑。这种方式无需修改业务代码,通过链接器实现全局拦截。

优点

  • 对业务代码透明,无需改动原有malloc/free调用
  • 可记录调用栈地址(通过__builtin_return_address
  • 适合大型项目集成

缺点

  • 实现相对复杂,需处理多线程、递归调用等问题
  • 涉及动态链接器,可移植性稍差

7.3.1实现代码

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdbool.h>
#include <link.h>
#include <sys/stat.h>

// 将地址转换为可执行文件内的相对偏移(用于符号解析)
static void *addr_to_offset(void *addr) {
    Dl_info info;
    struct link_map *link;
    if (dladdr1(addr, &info, (void **)&link, RTLD_DL_LINKMAP)) {
        return (void *)((size_t)addr - link->l_addr);
    }
    return addr;
}

typedef void *(*malloc_t)(size_t size);
typedef void (*free_t)(void *ptr);

malloc_t real_malloc = NULL;
free_t real_free = NULL;

// 防止递归调用的线程局部标志
static __thread bool in_hook = false;

static void ensure_block_dir() {
    struct stat st = {0};
    if (stat("./block", &st) == -1) {
        mkdir("./block", 0755);
    }
}

void *malloc(size_t size) {
    if (!real_malloc) {
        real_malloc = dlsym(RTLD_NEXT, "malloc");
    }

    // 如果已经在hook中,直接调用真实malloc,避免递归
    if (in_hook) {
        return real_malloc(size);
    }

    in_hook = true;
    void *ptr = real_malloc(size);
    
    if (ptr) {
        ensure_block_dir();

        void *caller = __builtin_return_address(0); // 获取调用malloc的地址
        void *offset = addr_to_offset(caller);

        char buff[256];
        snprintf(buff, sizeof(buff), "./block/%p.mem", ptr);

        FILE *fp = fopen(buff, "w");
        if (fp) {
            fprintf(fp, "[+][%p] %p %zu malloc\n", offset, ptr, size);
            fclose(fp);
        }
    }

    in_hook = false;
    return ptr;
}

void free(void *ptr) {
    if (!ptr) return;

    if (!real_free) {
        real_free = dlsym(RTLD_NEXT, "free");
    }

    if (in_hook) {
        real_free(ptr);
        return;
    }

    in_hook = true;

    char buff[256];
    snprintf(buff, sizeof(buff), "./block/%p.mem", ptr);

    if (unlink(buff) < 0) {
        // 可能重复释放或未记录
    }

    real_free(ptr);
    in_hook = false;
}

// 测试代码
int main() {
    int size = 5;
    void *p1 = malloc(size);
    void *p2 = malloc(size * 2);
    void *p3 = malloc(size * 3);

    free(p1);
    free(p3);  // p2 泄漏
    return 0;
}

然后通过addr2line -e ./memleak -f -a可以查看是哪里内存泄漏了:

请添加图片描述

8.总结

内存泄漏是C/C++程序员必须面对的挑战。通过宏定义或hook技术,我们可以实现轻量级的内存泄漏检测组件,在开发和测试阶段快速定位问题。对于线上服务,可以设计动态开关机制,在必要时开启检测,平衡性能与问题排查的需求。
https://blog.csdn.net/qq_57951250/article/details/159047819?spm=1011.2124.3001.6209

https://github.com/0voice

...全文
23 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

545

社区成员

发帖
与我相关
我的任务
社区描述
零声学院,目前拥有上千名C/C++开发者,我们致力将我们的学员组织起来,打造一个开发者学习交流技术的社区圈子。
nginx中间件后端 企业社区
社区管理员
  • Linux技术狂
  • Yttsam
  • 零声教育-晚晚
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

请新加入的VIP学员,先将自己参加活动的【所有文章】,同步至社区:

【内容管理】-【同步至社区-【零声开发者社区】

试试用AI创作助手写篇文章吧