为什么删除的内存无法被重复使用

7
我正在使用MSVC 9.0在Windows 7上进行C++开发,同时也在Windows XP SP3上测试和复现了该问题。当我分配1GB的0.5MB大小的对象时,删除它们后一切正常且表现符合预期。但是,如果我分配1GB的0.25MB大小的对象并删除它们,内存仍被保留(在Address Space Monitor中为黄色),并且从那时起只能用于小于0.25MB的分配。
通过更改typedef的结构体,可以使用此简单代码测试两种场景。在分配和删除这些结构体之后,将分配1GB的1MB char缓冲区,以查看char缓冲区是否使用了这些结构体曾经占用的内存。
struct HalfMegStruct
{
    HalfMegStruct():m_Next(0){}

    /* return the number of objects needed to allocate one gig */
    static int getIterations(){ return 2048; }

    int m_Data[131071];
    HalfMegStruct* m_Next;
};

struct QuarterMegStruct
{
    QuarterMegStruct():m_Next(0){}

    /* return the number of objects needed to allocate one gig */
    static int getIterations(){ return 4096; }

    int m_Data[65535];
    QuarterMegStruct* m_Next;
};

// which struct to use
typedef QuarterMegStruct UseType;

int main()
{
    UseType* first = new UseType;
    UseType* current = first;

    for ( int i = 0; i < UseType::getIterations(); ++i )
        current = current->m_Next = new UseType;

    while ( first->m_Next )
    {
        UseType* temp = first->m_Next;
        delete first;
        first = temp;
    }

    delete first;

    for ( unsigned int i = 0; i < 1024; ++i )
        // one meg buffer, i'm aware this is a leak but its for illustrative purposes. 
        new char[ 1048576 ]; 

    return 0;
}

以下是我使用 Address Space Monitor 的结果。请注意,这两个最终结果之间唯一的区别是分配给 1 GB 标记的结构体的大小不同Quarter Meg Half Meg 对我来说,这似乎是一个相当严重的问题,也许很多人可能正在遭受此类问题而并不知情。
  • 这是设计上的问题还是应该被视为错误?
  • 我能否使小型删除的对象实际上可以由更大的分配使用?
  • 更好奇的是,Mac 或 Linux 机器是否会遇到相同的问题?

1
闻起来像是内存碎片化,除了大小阈值。 - Ignacio Vazquez-Abrams
然而,内存碎片是指在已分配对象之间存在浪费空间。在上述代码中,所有对象都已被释放,因此内存碎片不可能是问题的原因。 - 0xC0DEFACE
你说保留内存只能用于分配小于0.25MB的对象。那么,当你分配许多较大的对象时,这个内存仍然被保留,你会得到一个内存不足的异常吗?我问这个问题是因为在Mac OS上,操作系统会保留未使用的内存以便更快地重新分配,除非另一个进程真正需要该内存。 - Björn Pollex
@Space_C0wb0y:是的,在使用这段内存之前,会抛出 out-of-memory 异常。 - 0xC0DEFACE
5个回答

9
我不能确定这是真的,但这看起来像是内存碎片(其一种形式)。分配器(malloc)可能会保留不同大小的桶以实现快速分配。当您释放内存时,它不会直接将其返回给操作系统,而是保留了桶,以便稍后可以从同一块内存处理相同大小的分配请求。如果是这种情况,该内存将可用于进一步的相同大小的分配请求。
通常对于大对象,此类优化会被禁用,因为即使未使用也需要预留内存。如果阈值介于您的两个大小之间,则可以解释该行为。
请注意,虽然您可能认为这很奇怪,但在大多数程序中(不是测试,而是实际应用),内存使用模式是重复的:如果您曾经请求过100k块,那么更有可能的情况是您会再次请求。保留内存可以提高性能并实际上减少来自同一桶中所有请求的碎片。
如果您想要投资一些时间,可以通过分析行为来了解分配器的工作原理。编写一些测试,获取大小X,释放它,然后获取大小Y,然后显示内存使用情况。固定X的值并尝试不同的Y值。如果两种大小的请求都从相同的桶中授予,则不会有保留/未使用的内存(左图像),而当大小从不同的桶中授予时,您将看到右侧图像上的效果。
我通常不编写Windows代码,甚至没有Windows 7,所以我不能确定这是真的,但它看起来确实是这样。

我原以为会是这样的,但我的理解是分配器应该将相邻的已释放内存块合并成更大的可用内存块,以便可以用于更大的分配。但这对我没有帮助,因为我在找到这个程序时,它正在加载一个巨大的XML文件,在加载过程中进行许多小的分配,然后在第二个加载阶段(加载在XML中指定的图像和网格(游戏资产))时耗尽了内存。 - 0xC0DEFACE

2

我可以确认在Windows 7下,使用g++ 4.4.0也会出现相同的行为,所以问题不在编译器上。实际上,当getIterations()返回3590或更多时,程序就会失败--你也遇到了同样的问题吗?这看起来像是Windows系统内存分配的一个bug。虽然有经验的人谈论内存碎片化很好,但是因为所有的东西都被删除了,所以观察到的行为绝对不应该发生。


不同的事情都被称为内存碎片。在一个天真的内存分配器中,所有请求都来自单个堆,内存碎片指的是无法分配的内存空洞,因为它们对于请求来说太小了。其他实现会为不同大小的对象保留不同的,试图减少碎片化,以便相同大小的新/删除/新/删除/新序列不会使内存碎片化。因为被保留在不同堆中而不可用的内存也被称为碎片化(另一种类型的)。 - David Rodríguez - dribeas
@David:就像我说的,所有东西都被删除了。内存分配器应该能够识别这一点,并在分配失败时释放中等大小对象堆(如果真的是这样)。你同意吗? - TonyK
1
@TonyK:没错,我同意一旦无法从可用内存中分配空间,应该尝试在不同的桶中重复使用当前进程中释放的内存。这个评论更多地是关于是否可以将其视为碎片化。如果你有1G的连续空闲内存,却无法分配1M,显然出了些问题!!! - David Rodríguez - dribeas
@David:再看一遍代码。它在结尾分配了1024个一兆字节的缓冲区,而不仅仅是一个。 - TonyK
@TonyK,我表述不够精确,但是有一个单一的分配正在失败,当这个分配失败时,仍然有大约1G的连续内存(从我在问题中理解的内容来看),但这只是一个细节。 - David Rodríguez - dribeas
@David:不,如果你运行这段代码是不会出现这种情况的。大部分的1GB内存都会在第二次分配成功。 - TonyK

1

使用您的代码,我执行了您的测试并得到了相同的结果。我怀疑在这种情况下David Rodríguez是正确的。

我运行了测试,并与您获得了相同的结果。似乎可能会出现“桶”行为。

我也尝试了两个不同的测试。而不是使用1MB缓冲区分配1GB数据,我按照内存首次删除后分配的方式进行了相同的分配。第二个测试中,我分配了半兆字节的缓冲区,清理后再分配四分之一兆字节的缓冲区,每个缓冲区都加起来达到512MB。最终,两个测试的内存结果相同,只分配了512个大块保留内存。

正如David所提到的,大多数应用程序倾向于进行相同大小的分配。然而,很明显可以看出这可能会成为一个问题。

也许解决这个问题的方法是,如果您以这种方式分配许多较小的对象,则最好分配一个大的内存块并自己管理它。然后当您完成时释放大块。


1

我与该领域的一些权威人士(如果你在这里,Greg,请打个招呼 ;D)交谈后,可以确认David所说的基本正确。

在分配约0.25MB对象的第一遍扫描中,堆正在保留和提交内存。在删除过程中缩小堆时,它以某种速度取消提交,但不一定释放在分配过程中保留的虚拟地址范围。在最后的分配遍历中,1MB的分配由于其大小而绕过了堆,因此开始与堆竞争VA。

请注意,堆是保留虚拟地址(VA)而不是将其提交。如果您好奇的话, VirtualAlloc VirtualFree可以帮助解释它们之间的区别。这个事实并不能解决你遇到的问题,即进程没有足够的虚拟地址空间

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接