65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (24投票s)

2008年4月16日

CPOL

5分钟阅读

viewsIcon

165845

downloadIcon

791

描述了一个用于跟踪堆内存分配和避免内存泄漏的模块

目录

引言

本文介绍了一种尝试解决大多数 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++ 的 newdelete 运算符。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;

现在,假设我们丢失了指针 pp1p2p3,我们该如何释放内存?内存跟踪库提供了许多方法。让我们找出如何。

方法 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;

在这种方法中,我们可以恢复大多数指针,但无法恢复指针 pp1,因为我们没有为它们分配名称。不过仍然有方法可以释放它们。让我们看看方法 2。

方法 2:释放组

// Deallocate group 0; p and p2
hmt_garbagecollect(0);
// Deallocate group 1; p1 and p3
hmt_garbagecollect(1);

在此方法中,我们释放组 1,因为 p1p3 被分配给了组 1,释放组 0,因为 pp2 没有被分配给任何组,因此它们属于组 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 日:原始文章
© . All rights reserved.