什么是内存泄漏?什么是内存碎片?有什么区别,内存泄漏如何排查?

张开发
2026/4/16 2:49:20 15 分钟阅读

分享文章

什么是内存泄漏?什么是内存碎片?有什么区别,内存泄漏如何排查?
什么是内存泄漏内存泄漏是指程序在动态分配内存后失去对该内存的引用或未能正确释放导致这块内存无法被再次使用或回收直到程序结束。典型后果程序占用的内存持续增长系统可用内存逐渐减少最终可能导致程序崩溃或系统卡死典型案例申请了未释放。void leak() { int *p malloc(sizeof(int)); // 分配内存 *p 10; // 忘记 free(p) — 函数返回后指针p销毁但内存未被释放 }什么是内存碎片内存碎片是指已分配的内存块之间存在的无法被有效利用的小块空闲内存。分为两类类型描述产生原因外部碎片空闲内存总量足够但分散在小块中无法满足大块连续分配请求频繁分配和释放不同大小的内存块内部碎片分配给程序的内存块比实际请求的大多余部分无法被利用内存分配器按固定大小如8、16、32字节分配示意图内存布局[已用] [空闲(8)] [已用] [空闲(16)] [已用] [空闲(4)] ↑ 无法分配连续20字节即使总空闲28字节 — 外部碎片 分配器给了32字节程序只用了20字节 → 12字节浪费 — 内部碎片外部碎片典型1.交替分配初始128KB 连续空闲 [ 空闲 128KB ] 1. 分配 30KB → 剩余 98KB连续 [ 30KB已用 ][ 空闲 98KB ] 2. 分配 40KB → 剩余 58KB连续 [30KB][ 40KB已用 ][ 空闲 58KB ] 3. 分配 20KB从剩余中取→ 剩余 38KB连续 [30KB][40KB][20KB][ 空闲 38KB ] 4. 释放 40KB 那块 [30KB][ 空闲 40KB ][20KB][ 空闲 38KB ] ↑ 两块空闲内存但被20KB已用块隔开 5. 请求分配 50KB - 总空闲 40 38 78KB ✅大于50KB - 最大连续空闲块 40KB ❌小于50KB → 分配失败这是外部碎片2.现实场景——文件系统磁盘块分配类FAT32碎片化 文件A: [块0] [块5] [块12] [块30] ← 物理上分散 文件B: [块1] [块6] [块13] 文件C: [块2] [块7] [块14] 空闲块: [3,4,8,9,10,11,15,16...] ↑ 连续块很少 要写一个需要3个连续块的新文件 → 可能失败即使总空闲块很多内部碎片定义分配给程序的内存块比实际请求的大块内部的剩余空间被浪费且无法被其他程序使用。例子1固定大小分配器最常见分配器--malloc分配申请时运行分配器// 假设内存分配器按 8、16、32、64 字节的固定大小分配 // 实际请求大小会被向上取整到最近的规格 请求 10字节 → 分配 16字节块 → 浪费 6字节内部碎片 请求 25字节 → 分配 32字节块 → 浪费 7字节 请求 33字节 → 分配 64字节块 → 浪费 31字节浪费率近50% 实际内存布局 [10B已用][6B浪费] ← 这6B无法被任何其他分配使用例子2结构体对齐编译器自动填充struct Example { char c; // 1字节 int i; // 4字节 → 编译器会在c后面填充3字节 short s; // 2字节 → 可能再填充2字节对齐到4的倍数 }; // 理论大小142 7字节 // 实际大小典型32位系统12字节 // 内部碎片5字节填充字节内存布局地址 0 1 2 3 4 5 6 7 8 9 10 11 [c][ 填充3 ][ i ][s][ 填充2 ] ↑ 实际数据 ↑ 内部碎片例子3页内碎片操作系统内存管理系统页面大小 4KB (4096字节) 进程请求 4000字节 - 操作系统分配 1页 4096字节 - 浪费 96字节页内碎片 进程请求 4097字节 - 分配 2页 8192字节 - 浪费 4095字节第二个页面只用了1字节例子4Slab分配器内核对象缓存https://download.csdn.net/blog/column/13043653/151306213// Linux内核为固定大小的对象维护slab // 例如task_struct 通常分配在 8KB 的slab中 kmalloc(5500, GFP_KERNEL); // kmalloc内部可能返回 8KB 的对象来自8192大小的slab // 浪费 ≈ 2500字节三、对比表格相同场景下的不同碎片场景外部碎片内部碎片原因分配/释放顺序不当分配粒度太粗或对齐要求空闲位置在已用块之间在已用块内部是否能被合并可以移动/整理已用块不能已经被分配出去了典型系统堆分配器malloc/free固定大小分配器、页分配器检测方式分配大块失败但总空闲够计算实际使用 vs 分配大小内存泄漏 vs 内存碎片核心区别对比维度内存泄漏内存碎片本质内存被占用且无法回收内存空闲但无法有效利用空闲内存实际空闲内存减少空闲内存总量不变但分散可用性内存完全丢失内存存在但不能满足大请求长期影响内存耗尽程序/OOM分配大块内存失败是否可恢复进程结束前不可恢复可通过压缩/整理恢复某些系统内存泄漏如何排查常用工具语言/平台推荐工具C/CValgrind, AddressSanitizer, Visual Studio Debug HeapJavaJProfiler, MAT, VisualVM, jcmdGopprof, go tool tracePythonmemory_profiler, tracemalloc, objgraph通用heaptrack, Intel Inspector典型排查步骤1. 使用静态分析/编译期检测# C/C 使用 AddressSanitizer (编译时加标志) gcc -fsanitizeaddress -g myprog.c -o myprog ./myprog # 退出时会报告泄漏位置2. 运行时动态检测Valgrind 示例valgrind --leak-checkfull --show-leak-kindsall ./myprog输出会显示definitely lost明确泄漏指针丢失indirectly lost间接泄漏如丢失了指向结构体的指针结构体内指针也丢失possibly lost可能泄漏指针仍存在但指向块内部3. 堆转储分析4. 代码审查常见泄漏模式每个malloc/new是否有对应的free/delete异常处理路径中是否释放内存容器中存储的指针是否在对象销毁时被清理循环引用引用计数语言如 Python/Swift5.其他方式缺点程序会变得极慢类似 Valgrind但比 Valgrind 灵活。场景 2已经知道大概泄漏的变量确认它是否真的没被释放(gdb) p ptr_to_object (gdb) watch -l ptr_to_object # 监视这个指针的变化看看谁最后修改了它导致丢失了释放机会。场景 3定位“为什么这块内存没被 free”(gdb) bt # 查看当前调用栈 (gdb) info locals # 查看局部变量中有没有应该释放的指针使用 GDB 脚本 断点记录分配/释放bash(gdb) break malloc (gdb) commands printf malloc %p\n, $rax continue end (gdb) break free (gdb) commands printf free %p\n, $rsi continue end然后运行一段时间对比哪些地址只分配没释放。mtrace这是glibc自带的一个轻量级内存泄漏检测工具。通过在代码中包含mcheck.h并调用mtrace()它可以将malloc/free的调用记录到文件中然后用脚本分析。优点是简单、无需额外安装但功能相对基础。memleak(BPF Compiler Collection, bcc)一个基于 LinuxeBPF的动态追踪工具。它的强大之处在于可以实时追踪正在运行的进程而无需停止或重启程序。这对于分析线上服务的内存增长问题非常有用。安装 bcc-tools 后即可使用memleak -p PID。GDB 查泄漏场景 1怀疑内存增长但 Valgrind 跑不起来程序太大 / 有实时性要求 / 不能慢几十倍用 GDB attach反复call malloc_stats()观察in use bytes是否持续增长提升 GDB 能力的技巧使用 GDB 脚本 断点记录分配/释放(gdb) break malloc (gdb) commands printf malloc %p\n, $rax continue end (gdb) break free (gdb) commands printf free %p\n, $rsi continue endgdb可用步骤1. 查看当前堆内存使用情况通过调用malloc_stats或mallinfo查看堆状态。bashgdb -p PID (gdb) call malloc_stats() # 输出 # Arena 0: system bytes 1048576, in use bytes 524288 ... # 可以看到总分配、空闲等统计或者(gdb) call (void)mallinfo() // 查看 arena, ordblks, uordblks, fordblks 等字段2. 遍历堆块高级/困难理论上 GDB 可以遍历malloc管理的所有堆块但非常麻烦因为你需要知道 glibc 的内部结构malloc_chunk。示例glibc 下手工看某个 arenabash(gdb) p main_arena (gdb) p *main_arena.top但这要求你对 ptmalloc 实现很熟悉不推荐。3. 检查哪些指针仍指向堆内存这是最实用的手动方法查看程序中的全局变量、栈上的指针看是否还指向那些“应该被释放但没释放”的内存。bash(gdb) info variables # 查看所有全局/静态变量 (gdb) p global_ptr # 查看指针值 (gdb) x/10x global_ptr # 查看指向的内存内容如果某个堆地址例如0x602010仍然存在指针引用但程序逻辑上已经不再需要它 →那就是泄漏点。快速自查清单内存使用量是否随时间线性增长监控工具top,htop,Prometheus长时间运行后是否出现bad_alloc/OutOfMemoryErrorOOM*重现问题时能否通过减少分配次数缓解是否有明显的“只分配不释放”的代码路径总结问题一句话解释解决方案内存泄漏忘记释放不再使用的内存配对分配/释放、使用智能指针、工具检测内存碎片内存被切碎大块分配失败使用内存池、对象池、改变分配模式两者往往同时发生泄漏减少了总可用内存碎片让剩余内存更难有效使用。排查时建议先用工具定位泄漏再考虑碎片优化。

更多文章