检查和释放内存泄漏






4.67/5 (9投票s)
2003年7月18日
10分钟阅读

154951

3133
关于检测和释放内存泄漏的文章
引言
此代码片段可用于检测和释放 C 程序中的内存泄漏。目前不适用于 C++。它速度快,额外内存开销小,尤其是在线程争用不常见的情况下。这意味着它可以用于发布版本,自动释放其他开发者编写的 C DLL 和程序中的内存泄漏。这避免了因编辑代码修复泄漏而可能引入新错误的风险。
在诊断模式下,它会报告泄漏发生处的行号和文件名。这会平均为每次内存分配增加 22 字节。由于 MSVC 已经可以做到这一点,而且大多数编译器可能也可以,我认为这里的主要优点是你可以自定义代码,- 而且你也可以在发布版本中使用它来测试内存泄漏,以防发布版本与调试版本有显著差异。
在自动释放模式下,你可以调用它来释放所有已分配的内存。在这种情况下,它平均会为每次内存分配增加 10 字节。这对于 DLL 来说可能最有用了,- 如果分配是为 DLL 的每个调用单独进行的,你可以在 DLL 中的例程返回时将其设置为自动释放内存。
它实现了 malloc
、realloc
、calloc
和 strdup
。将其扩展到更少见的内存分配例程,如 _expand
,以及 strdup
的宽字符串版本,应该很容易。
注意
你不希望将此库与普通的 malloc
和 free 使用混合,因为当它接收到一个要释放的指针时,它首先会做的就是将指针递减并访问递减后的指针以查找记录号(这项技术使其速度如此之快)。如果指针最初是通过正常使用 malloc
分配的,这将导致访问冲突。反之,如果你尝试使用正常的 free 来释放其指针之一,因为在将指针传递给调用应用程序之前会对其进行递增,你也会在那里遇到错误。
使用此库进行的每次分配都需要使用此库来释放,并且它应该只用于释放它自己分配的内存。但是,你可以通过仅为这些部分包含其头文件来将其用于源代码的特定部分。或者,定义 code_uses_redefined_malloc
,然后将 malloc
替换为 _malloc_
,calloc
替换为 _calloc_
,free
替换为 _free_
,strdup
替换为 _strdup_
,涵盖你想要使用它的整个部分。
另外请务必注意 - 它适用于依赖 malloc
等进行内存分配的纯 C 程序。它不会调用 Dtor,也不适用于 new
。
背景
内存泄漏是 24/7 运行的程序和 DLL 中的一个问题。此库可能有助于在这些情况下自动释放内存。尤其是在 DLL 是将最初依赖操作系统在程序退出时自动释放内存的程序转换而来时,它可能会很有用。你只需要包含头文件,无需编辑源代码并可能引入新错误。它还可以用于内存泄漏诊断,以定位发生泄漏的位置。
使用代码
如何使用 - 通用
像往常一样使用你的内存分配例程 - 然后当你释放了所有将要释放的内存后,调用 MemLeakCheckAndFree()
如果它发现任何尚未分配的指针,它将显示诊断报告 - 并将它们释放。如果你关闭了诊断功能,那么这个例程就是为你自动释放内存的。
在诊断模式下,警告会写入 stderr
,这对于控制台应用程序很有用。但是,如果你将其与 Windows 程序一起使用,你将需要创建自己的回调例程来处理警告。头文件中包含了示例回调,你可以使用它们。
SetMemLeakWarnProc(MemLeakWarnProc);
和
SetMemLeakLogProc(MemLeakLogProc);
MemLeakCheckAndFree
使用日志例程,因为可能需要显示一个可能很长的警告列表 - 通常可能会将这些写入文件。这是它的输出可能的样子
************************************************************
2 PM Saturday, July 12, 2003 GMT Daylight Time
************************************************************
Memory Leak!
File: I:\Lissajous\main.c, line 46, size 100
File: I:\Lissajous\main.c, line 248, size 1000
它还在最后调用一次 MemLeakWarnProc
,以提醒用户发生了内存泄漏。
作为 DLL 使用
链接到相关的 DLL,例如用于诊断的 MemLeakCheckt.dll,或仅用于自动释放的 MemLeakCheckat.dll(将相关的 .lib 文件添加到你的链接器选项中)。
现在,在所有标准 include 之后,并在分配和释放发生的源代码之前,包含头文件。
在头文件中,定义
#define LINK_MEMLEAK_CHECK_AS_DLL
然后,如果你链接的是 MemLeakCheckt.dll(包含诊断功能的那个),则需要定义
#define cl_mem_leak_check #define cl_mem_leak_diagnostics
或者对于 MemLeackCheckat.dll,只需定义 cl_mem_leak_check
由于你可能使用与 DLL 不同的 C 运行时库版本(无论是单线程还是多线程,发布还是调试),因此最好将此代码添加到你的一个源文件末尾,在所有内存分配之后,其中 #undef
s 不会影响它们。然后在应用程序启动时调用它,为 DLL 的内存分配例程设置为你 CRT 的例程。
#ifdef cl_mem_leak_diagnostics #undef malloc #undef calloc #undef realloc #undef memcpy #undef free #include "malloc.h" void init_memleakcheck_alloc(void) { set_mlc_malloc(malloc); set_mlc_calloc(calloc); set_mlc_realloc(realloc); set_mlc_free(free); set_mlc_memcpy(memcpy); set_mlc_strlen(strlen); set_mlc_strcpy(strcpy); } #endif
我不知道此步骤是否必要。对于文件指针来说是需要的,因为如果你在使用了与调用应用程序不同 CRT 版本的库中使用了文件指针,那么它将不再有效,并且很可能导致访问冲突,而此方法可以在这种情况下解决问题。但是 malloc
和 free 指针在调用应用程序中似乎工作正常。即使你不这样做。但我认为最好还是这样做。欢迎提供有关此事的任何信息 :-)。
如果你想在每次例程退出时为 DLL 自动释放内存,那么我认为最好将其作为源代码包含并使用线程局部变量 - 请参阅下一节。
作为源代码添加到你的项目中
将源代码添加到你的项目中,并将头文件添加到所有 include 之后,并在使用 malloc
等之前。此头文件将重新定义 malloc
、realloc
、calloc
和 strdup
,以便使用 MemLeakCheck
中的版本而不是标准版本。这些版本接着又会调用标准的 malloc
等来进行实际的内存分配。
如前所述,定义
#define cl_mem_leak_check #define cl_mem_leak_diagnostics
并确保注释掉构建 DLL 或链接到 DLL 所需的两个定义。
如果你想在每次例程退出时为 DLL 自动释放内存,那么最好使用线程局部变量 - 以防多个线程同时想要使用同一个例程。
当你使用线程局部变量时,每个线程必须单独调用自动释放例程。在你每次退出特定例程时释放内存的情况下,这很容易,因为每次它只会为它被调用的特定线程释放内存。
要使用线程局部变量,请在此头文件中定义
#define MLC_USE_THREAD_LOCAL_VARIABLES
关注点
其思想是使用每次分配的前几个字节来存储你想要的分配信息 - 在诊断模式下,它有一个指向文件名静态字符串的指针以及行号。在两种模式下,它还有一个记录号,该记录号是数组中指针的索引。
然后,当需要分配内存时,你会分配所需的内存加上一点额外的空间用于此额外信息,并返回一个递增的指针,该指针指向记录末尾的数据本身。当需要释放内存时,你会递减调用应用程序提供给你的指针,以查找其记录和记录号。
用于提高速度的特殊技巧是在此处添加记录号。当需要释放内存时,可以直接访问指针数组中的相关条目,无需查找或进行任何类型的搜索。然后,当你释放内存时,你将该条目设置为 NULL
,以表明该指针不再处于活动状态。你需要不时地压缩指针数组 - 只需从头开始遍历它,并将所有非空指针移回,然后访问它们并相应地更新它们的记录号。
为了处理线程争用,由于预计这种情况很少发生,它使用了 InterlockedExchange
结合高效的忙等待(通过 Sleep(0)
放弃每个时间切片的剩余部分,直到获得访问权限)。这非常快,只需要执行单个汇编指令即可获得对该部分的访问权限,比临界区更好。
如果争用频繁发生,那么这意味着你的一个线程反复调用 Sleep(0)
,而另一个线程正在分配内存。这不应该有太大的性能影响 - 重复休眠的线程在获得内存之前无法做任何事情。它也不会影响其他可能正在运行的程序,因为它在每个时间切片只使用几个汇编指令。所以 - 我看不出这会产生多少性能影响 - 如果有人尝试将其用于具有高内存分配线程争用的应用程序,请纠正我,我将对它在这种情况下的表现感兴趣。如果发现它有性能影响,那么一个解决方案是使用线程局部变量,并为每个争用的线程分别进行自动释放。
内存和时间要求都很低。但是,它会不时地通过遍历数组来压缩指针数组。这包括检查 n 个指针的数组中的每个条目,其中 n 至少为 1000,最多是你当前使用但尚未释放的分配数量的 1.5 倍。所以我认为在大多数情况下,这并不是太大的时间开销。
如果同时有数万或数十万个小型分配,并且应用程序对时间要求非常高,那么压缩指针数组所需的时间可能会很可观。在这种情况下,你可能希望在库中添加一个例程,以便在进行时间关键工作时暂时暂停数组压缩,并在稍后合适的时机恢复它(一个简单的修改)。也许没有什么可以加速它的,因为这种方法的想法是使分配和释放即时完成,甚至不需要像在二叉树中插入条目那样多的时间开销,而我认为这比消除数组压缩步骤更重要。然后,要压缩数组,我很难看到如何能比从头开始简单的数组遍历做得更好,而又不增加内存需求或减慢元素添加到其中的速度。同样,如果有一种改进方法,请务必纠正我 - 我将非常乐意听到任何改进。
有关更多信息,请参阅 Mem Leak Check 主页。
历史
- 更新 更新了描述以纠正早期版本。我曾以为程序退出时需要释放内存,但似乎 Windows 会自动完成 - 请参阅讨论。添加了关于其用于特定部分的说明,因为你可能会在 24/7 型 DLL 中使用它。
- 首次发布