堆内存管理器和垃圾收集器






4.88/5 (24投票s)
描述了一个用于跟踪堆内存分配和避免内存泄漏的模块
目录
引言
本文介绍了一种尝试解决大多数 C++ 程序员痛恨的常见问题的方法:内存泄漏和内存溢出。本文使用的方法是跟踪程序分配的所有内存。它还提供基本的保护检查,以确定写入分配块的内存是否已超出实际分配的字节数。
此方法还允许您按 ID 或名称组织和分组分配的内存。为内存分配命名具有优势,因为您可以在不进行指针传递的情况下按名称获取已分配的内存。为分配分配组 ID 有助于程序员对内存分配进行分组,从而可以调用单个函数来释放某个组的所有内存。
为什么要跟踪内存?
跟踪内存是监视所有已分配内存的非常有效的方法。这使您能够稍后枚举所有已分配的内存块,并释放所有已分配的内存。这是垃圾收集的实现。
为什么要进行垃圾收集?
垃圾收集在现代编程语言(如所有 .NET 语言和 Java)中得到了广泛应用。在 C++ 中实现垃圾收集并非没有代价,但它始终是完全消除内存泄漏的一种安全方法。
问题
分配内存时,总会在某个时候需要将其释放。无论是由于编程错误还是程序逻辑,都很容易忘记释放内存。示例
void ServePizzas(int pizzacount)
{
char* p = new char[pizzacount];
// ... more code here...
if (g_remainingpizzas == 0) return;
// ... more code here...
delete [] p;
}
在上面的代码中,通过 char* p = new char[pizzacount];
语句分配了一个内存块。中间的条件语句包含一个 return
语句,导致函数退出,并且其后的所有代码都不会执行。由于 delete [] p;
语句不会执行,因此分配的内存不会被释放,从而导致内存泄漏。
提出的解决方案
本文提出的解决方案涉及重载 C++ 的 new
和 delete
运算符。new
运算符通常如下调用:
char* p = new char[256];
这会在指针 p
中分配一个 256 字节的数组。使用此库时,调用保持不变,但内部已标记并跟踪了内存,从而可以控制已分配的内存。
new
运算符也可以带有参数,在此库中,new
运算符具有三个带参数的重载。让我们看一些示例:
// Allocate a 256 byte array using normal new operator.
char* p = new char[256];
// Allocate a 256 byte array with group ID 1.
char* p1 = new(1) char[256];
// Allocate a 256 byte array with name "my array"
char* p2 = new("my array") char[256];
// Allocate a 256 byte array with name "his array" with group ID 1.
char* p3 = new(1, "his array") char[256];
以上都是分配内存的方式。要释放前面示例中分配的内存,我们通常会这样做:
delete [] p;
delete [] p1;
delete [] p2;
delete [] p3;
现在,假设我们丢失了指针 p
、p1
、p2
和 p3
,我们该如何释放内存?内存跟踪库提供了许多方法。让我们找出如何。
方法 1:恢复指针
// We cannot recover p since it is not named.
char* p = Null;//???
// We cannot recover p1 since it is not named.
char* p1 = Null;//???
// Recover p2:
char* p2 = hmt_getnamed("my array");
delete [] p2;
// Recover p3:
char* p3 = hmt_getnamed("his array");
delete [] p3;
在这种方法中,我们可以恢复大多数指针,但无法恢复指针 p
和 p1
,因为我们没有为它们分配名称。不过仍然有方法可以释放它们。让我们看看方法 2。
方法 2:释放组
// Deallocate group 0; p and p2
hmt_garbagecollect(0);
// Deallocate group 1; p1 and p3
hmt_garbagecollect(1);
在此方法中,我们释放组 1,因为 p1
和 p3
被分配给了组 1,释放组 0,因为 p
和 p2
没有被分配给任何组,因此它们属于组 0。让我们看看更粗暴的方法。
方法 3:释放所有
// Deallocate all memory allocated by new.
hmt_garbagecollect();
在此方法中,我们强制释放由 new
运算符分配的所有内存,甚至包括使用普通 new
运算符分配的 p
。这可以确保没有内存泄漏。
更多信息
如果您需要知道程序分配了多少内存,可以使用以下代码:
size_t memallocated = hmt_getallocated();
printf("Memory allocated using new: %d", memallocated);
如果您需要打印关于所有已分配内存的调试信息:
hmt_debugprint();
如果您想打印调试信息,以查看是否有任何内存块被溢出或损坏:
hmt_debugcheck();
实现细节
由于我们重载了 new
语句,因此我们可以控制分配的内存量。分配的内存会比实际请求的多一些,以便为跟踪数据腾出空间。
对于 32 位处理器,每次内存分配的开销是 32 字节;对于 64 位处理器,开销是 64 字节。下表代表调用 new
语句时已分配块的内部内存结构。这也说明了内存跟踪器如何知道所有已分配的内存。
字节数 | 字段 | 描述 |
4 字节 | 垃圾 UID | 用于确定内存是否已被覆盖 |
4 字节 | Group ID | 已分配块的组 ID |
4 字节 | 名称的哈希值 | 如果项目已命名,此处将包含名称的哈希值 |
4 字节 | 标志 | 描述内存块的内部标志 |
指针 * | 大小 | 块的大小(包括开销) |
指针 * | 前一个块 | 指向前一个已分配块的链表指针 |
指针 * | 下一个块 | 指向下一个已分配块的链表指针 |
n 字节 | 已分配内存 ** | new 语句请求的实际内存 |
4 字节 | 垃圾 UID | 用于确定内存是否已被覆盖 |
* 对于 32 位处理器为 4 字节,对于 64 位处理器为 8 字节。
** new
语句返回的指针指向此内存块。
通过此结构,所有内存块都被知晓并链接在一起。
结论
此模块是一个更大、更复杂的内存池模块的一部分,该模块将在以后发布。此模块肯定有改进的空间,其中之一是名称查找,这是一个线性搜索,因此当内存分配数量很大时速度很慢。在内存池模块中,使用了二叉树来加快速度,但这将在以后讨论。
此模块绝不是执行此类内存跟踪的唯一方法,但它是消除内存泄漏的一种非常有效的方法。
修订历史
- 2008 年 4 月 16 日:原始文章