内存泄漏
内存泄露
1️⃣ 什么是内存泄漏?
在 C++ 中,内存泄漏 指的是程序通过 new、malloc 等方式向 堆区 申请了内存,却在不再需要时 没有及时释放,或者由于 指针丢失、异常中断、所有权管理混乱 等原因,导致这块内存再也无法被程序有效访问和回收。
2️⃣ 内存泄漏的危害
- 性能退化: 泄漏内存会持续占用物理内存,当物理内存不足时,系统会启用
swap分区,磁盘IO速度远低于内存,导致程序响应变慢、CPU等待时间增加。 - 程序崩溃: 泄漏内存耗尽系统可用内存(包括
swap)时,操作系统会触发 “内存溢出(OOM)”,强制终止占用内存最多的进程。 - 资源耗尽: 对于长期运行的服务(如服务器程序),泄漏会持续累积,可能导致服务在运行一段时间后突然宕机,且难以复现根因。
3️⃣ 内存泄漏的分类
对于 Valgrind 来说,其主要将 内存泄漏 分为四类:
Definitely Lost:- 定义: 没有指针
p可以指向一块已经分配的堆内存,100%确定为泄漏;
- 定义: 没有指针
Indirectly Lost:- 定义: 子对象本身没有直接丢失指针,但由于父对象已泄漏,导致它也随之不可访问;
Possibly Lost:- 定义: 模糊指针引用,比如
p一开始指向堆区的开头,但是之后对p进行了移动,p指向内存中间位置;
- 定义: 模糊指针引用,比如
Still Reachable:- 定义: 程序在运行过程中申请了一块堆内存,并始终由 全局指针 或 静态指针 保存其地址。
4️⃣ 内存泄漏和内存溢出的区别
| 对比维度 | 内存泄漏(Memory Leak) | 内存溢出(Out Of Memory, OOM) |
|---|---|---|
| 触发条件 | 长期运行中内存渐进累积,无立即风险 | 单次内存分配超过系统可用内存 |
| 表现形式 | 程序运行越久,内存占用越高,最终可能引发 OOM |
立即崩溃,报错 Cannot allocate memory 或被 OS 杀死 |
| 根因 | 内存未释放(如 new 后无 delete、循环引用) |
内存分配需求超过系统上限(如数组过大申请、数据量过大) |
| 解决方向 | 排查未释放的内存分配点,修复释放逻辑 | 优化内存分配方式(如分块分配、减少单次申请量) |
5️⃣ 常见原因
new/delete不匹配方式:
new []分配内存,用delete释放根因:
new[]会在堆内存头部存储 “数组元素个数”;delete[]会读取该值并释放所有元素;delete会忽略该信息,仅释放指针指向的单个对象,导致数组剩余元素泄漏。
案例
1
2
3
4
5int main() {
int* arr = new int[10]; // 分配数组,内存中会存储数组大小(供delete[]使用)
delete arr; // 错误:用delete释放数组,仅释放第一个元素,其余9个元素泄漏
return 0;
}
异常处理引发的泄漏
根因: 异常抛出时,程序会跳过当前函数的后续代码(包括资源释放逻辑),若未用
RAII保护资源,会直接导致泄漏。案例
1
2
3
4
5
6
7
8
9
10
11
12void leakOnException() {
int* data = new int[100];
try {
// 模拟异常抛出(如文件打开失败、网络错误)
throw std::runtime_error("error");
// 异常抛出后,以下 delete 永远不会执行
delete[] data;
} catch (...) {
// 未处理 data 的释放,直接重新抛出
throw;
}
}
智能指针使用陷阱
- 根因:
shared_ptr循环引用,直接导致内存泄漏;weak_ptr未调用expired()来检查shared_ptr是否存在,而是直接将weak_ptr通过lock()直接返回,并将返回的结果直接解引用,使得直接崩溃;shared_ptr初始化陷阱,使用多个shared_ptr直接管理同一个对象,使其进行double free;
- 根因:
STL容器与指针的隐患- 根因:
STL容器存储了 指向堆内存的指针,但是 未遍历释放;STL容器保存了 动态数组;STL临时容器保存 指针;
- 根因:
全局/静态变量泄漏
- 根因:
- 全局变量/静态变量 的生命周期与程序一致,若其内部持有 动态资源(如指针、容器),程序退出前未清理,会导致泄漏(工具通常标记为
Still Reachable)
- 全局变量/静态变量 的生命周期与程序一致,若其内部持有 动态资源(如指针、容器),程序退出前未清理,会导致泄漏(工具通常标记为
- 根因:
6️⃣ 排查手段
面对复杂的泄漏场景,手动排查几乎不可能,需依赖专业工具。
1️⃣ Valgrind
Valgrind 是 Linux 下最经典的内存调试工具,核心组件 memcheck 可检测 内存泄漏、越界访问、双重释放 等问题,精度极高。
原理
Valgrind 并非直接运行程序,而是将程序翻译成 “中间代码”,在模拟 CPU 环境中执行。
通过跟踪每一块堆内存的 “分配记录”(分配地址、大小、调用栈)和 “引用状态”(是否有指针指向),在程序退出时对比 “已分配未释放” 的内存,确定泄漏类型和位置。
常用参数详解
--leak-check=full:- 作用: 开启完整泄漏检测;
- 示例: 排查生产环境泄漏时,需用
valgrind --leak-check=full ./program
--log-file=leak.log:- 作用: 将检测报告输出到文件,避免终端刷屏,便于后续分析;
- 示例: 长时间运行程序时,用
--log-file=leak.log保存报告;
--track-threads=yes- 作用: 跟踪线程创建与销毁,关联线程
ID与内存分配(多线程场景必备); - 示例: 排查多线程泄漏时,加此参数:
--track-threads=yes;
- 作用: 跟踪线程创建与销毁,关联线程
优缺点
- 优点: 精度极高、支持多线程、无需修改代码;
- 缺点: 性能损耗大(程序运行速度降低
10~50倍)、内存开销高(需额外占用2~3倍内存),不适合生产环境实时检测。
建议
用 Valgrind 运行优化后的程序(-O2/-O3):优化会导致代码重排,调用栈信息不准确,建议用 -O0 编译。
2️⃣ AddressSanitizer
ASan 是 Google 开发的内存错误检测工具,集成在 GCC/Clang 中,相比 Valgrind 速度更快,适合开发调试阶段使用。
原理
ASan 的核心是 “影子内存”(Shadow Memory):
- 将程序的每
8字节内存映射为1字节影子内存,用于标记 “内存是否可访问”(如已分配、已释放、越界)。 ASan在编译阶段对内存访问代码(load/store)进行 “插桩”,每次访问内存前先检查影子内存状态,若非法则触发错误报告。
ASan 会在程序退出时扫描所有堆内存,对比 “已分配未释放” 的块,生成 泄漏报告。
使用流程
- 编译阶段: 添加
-fsanitize=address -g选项 - 链接阶段: 若需静态链接
ASan(避免依赖系统ASan库),加-static-libasan - 运行阶段: 设置环境变量
ASAN_OPTIONS配置检测行为;
优缺点
- 优点:
- 速度快(仅比正常程序慢 2~5 倍)
- 内存开销可控(额外占用 2~3 倍内存)
- 支持多线程,适合开发调试;
- 缺点:
- 不支持区分泄漏类型
- 部分场景存在误报
项目案例
面试问题:你了解过内存泄漏吗?
- 我对内存泄漏的理解是:程序向堆区申请了资源,但因为 释放逻辑缺失、异常中断、所有权管理混乱 或者 智能指针循环引用,导致这块内存无法再被正常回收。
- 对服务端程序来说,它最危险的点在于不会立刻炸,而是随着运行时间不断积累,最终表现为内存持续上涨、响应变慢,严重时触发
OOM。 - 我在自己实现
sylar协程服务器的早期阶段,框架还不完整,当时先写了一个测试服务器去验证IOManager + HttpServer + Servlet这条链路。跑测试时我发现进程内存会持续上涨,后来用Valgrind和ASan去排查,最后定位到一段临时测试代码里的资源释放遗漏,并把它改成了RAII。 - 这个过程让我比较系统地理解了:内存泄漏不只是 “忘了 delete”,更核心的是生命周期和所有权设计是否清晰。
我是怎么发现这个问题的?
第一步:先看到现象
我的第一反应不是直接上工具,而是先观察现象:
- 测试服务器功能上没有问题,接口能正常返回;
- 但我连续多次访问测试接口后,进程内存占用不降反升;
- 重复压测一段时间后,内存曲线持续上涨。
这时我初步怀疑:
- 是不是某个请求上下文对象没有释放;
- 是不是容器里留了裸指针;
- 是不是异常或早返回路径把释放逻辑跳过去了。
第二步:构造稳定复现
反复访问测试接口:如果此时观察到内存持续增长,这个问题就具备排查价值了。
使用工具排查
方式一:Valgrind
生成
Debug模式的可执行程序;1
2
3
4cmake -S /root/workspace/sylar -B /root/workspace/sylar/build-debug \
-DCMAKE_BUILD_TYPE=Debug
cmake --build /root/workspace/sylar/build-debug -j用
Valgrind运行测试程序1
2
3
4
5
6valgrind \
--leak-check=full \
--show-leak-kinds=all \
--track-origins=yes \
--log-file=leak.log \
/root/workspace/sylar/bin/exe/my_http_server使用
Jmeter进行压测生成的日志文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14==104819== 131,408 (168 direct, 131,240 indirect) bytes in 3 blocks are definitely lost
==104819== by 0x2984E0: ... (test_memory_leak_server.cpp:104)
==104819== 84 bytes in 1 blocks are definitely lost
==104819== by 0x298544: ... (test_memory_leak_server.cpp:105)
==104819== 65,536 bytes in 1 blocks are definitely lost
==104819== by 0x298583: ... (test_memory_leak_server.cpp:106)
==104819== LEAK SUMMARY:
==104819== definitely lost: 65,788 bytes in 5 blocks
==104819== indirectly lost: 131,240 bytes in 4 blocks
==104819== possibly lost: 0 bytes in 0 blocks
==104819== still reachable: 0 bytes in 0 blocks
