为什么在分配/解除分配大量小对象后内存不可重用?

时间:2013-10-29 08:53:53

标签: c++ vector memory-leaks stl crt

在我们的某个项目中调查内存链接时,我遇到了一个奇怪的问题。不知何故,当父容器超出范围并且除了小对象之外不能使用时,为对象分配的内存(shared_ptr到object的向量,见下文)没有完全回收。

最小的例子:当程序启动时,我可以毫无问题地分配一个1.5Gb的连续块。在我稍微使用内存后(通过创建和破坏一些小对象),我不能再进行大块分配。

测试程序:

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class BigClass
{
private:
    double a[10000];
};

void TestMemory() {
    cout<< "Performing TestMemory"<<endl;
    vector<shared_ptr<BigClass>> list;
    for (int i = 0; i<10000; i++) {
        shared_ptr<BigClass> p(new BigClass());
        list.push_back(p);
    };
};

void TestBigBlock() {
    cout<< "Performing TestBigBlock"<<endl;
    char* bigBlock = new char [1024*1024*1536];
    delete[] bigBlock;
}

int main() {
    TestBigBlock();
    TestMemory();
    TestBigBlock();
}

如果在循环中使用带有new / delete或malloc / free的普通指针而不是shared_ptr,问题也会重复。

罪魁祸首似乎是在TestMemory()之后,应用程序的虚拟内存保持在827125760(无论我调用它的次数如何)。因此,没有足够的免费VM regrion可容纳1.5 GB。但我不确定为什么 - 因为我肯定会释放我用过的内存。是否有一些“性能优化”CRT可以最大限度地减少操作系统调用?

环境是Windows 7 x64 + VS2012 +没有LAA的32位应用程序

4 个答案:

答案 0 :(得分:2)

很抱歉发布了另一个答案,因为我无法发表评论;我相信很多其他人都非常接近答案: - )

无论如何,罪魁祸首很可能是地址空间碎片化。我知道你在Windows上使用Visual C ++。

C / C ++运行时内存分配器(由malloc或new调用)使用Windows堆来分配内存。 Windows堆管理器有一个优化,它将在一定大小限制下保留块,以便在应用程序稍后请求相似大小的块时能够重用它们。对于较大的块(我无法记住确切的值,但我猜它大约是一兆字节),它将直接使用VirtualAlloc。

其他具有许多小分配模式的长期运行的32位应用程序也存在这个问题;让我意识到这个问题的是MATLAB - 我正在使用&#39;单元阵列&#39;功能基本上分配了数百万个300-400字节块,即使在释放它们之后也会造成这个地址空间碎片问题。

解决方法是使用Windows堆函数(HeapCreate()等)创建私有堆,通过它分配内存(根据需要将自定义C ++分配器传递给容器类),然后在销毁时删除该堆你想要内存回来 - 这也有一个快乐的副作用,就是在一个循环中删除()删除(以及非常多的块)。

重新。 &#34;记忆中剩下的东西&#34;首先引起这个问题:在记忆中没有任何东西可以留下来&#39;本身,更多的情况是被释放的块被标记为空闲但没有合并。堆管理器有一个地址空间的表/映射,它不允许你分配任何会强制它将自由空间合并为一个连续块的东西(可能是性能启发式)。

答案 1 :(得分:1)

这不是内存泄漏。使用的内存U由C \ C ++ Runtime分配。运行时从OS应用大量内存,然后每个新调用的内存将从该大容量内存中分配。当删除一个对象时,Runtime不会立即将内存返回给操作系统,它可以保存该内存以进行性能。

答案 2 :(得分:1)

这里没有任何东西表明真正的“泄漏”。您描述的内存模式并不出乎意料。以下是可能有助于理解的几点。发生的事情是高度依赖操作系统。

  • 程序通常只有一个堆,可以扩展或缩小。然而,它是一个连续的内存区域,因此更改大小只会改变堆末尾的位置。这使得很难将内存“返回”到操作系统,因为即使该空间中的一个小小物体也会阻止其缩小。在Linux上你可以查找函数'brk'(我知道你在Windows上,但我认为它做了类似的事情)。

  • 大量分配通常采用不同的策略。不是将它们放在通用堆中,而是创建一个额外的内存块。当它被删除时,这个内存实际上可以“返回”到操作系统,因为它保证什么都没有使用它。

  • 大块未使用的内存不会消耗大量资源。如果您通常不再使用内存,则可能只是将其分页到磁盘。不要假设因为某些API函数表示您正在使用内存而实际上正在消耗大量资源。

  • API并不总是报告您的想法。由于各种优化和策略,实际上可能无法确定在特定时刻系统正在使用和/或可用的内存量。除非您有关于操作系统的详细信息,否则您无法确定这些值的含义。

前两点可以解释为什么一堆小块和一个大块会导致不同的内存模式。后面的要点说明了为什么这种检测泄漏的方法没有用处。要检测真正的基于对象的“泄漏”,通常需要一个专用的分析工具来跟踪分配。


例如,在提供的代码中:

  1. TestBigBlock分配和删除数组,假设这使用一个特殊的内存块,因此内存返回给操作系统
  2. TestMemory扩展了所有小对象的堆,并且永远不会将任何堆返回给操作系统。这里的堆完全可以从应用程序的角度来看,但从操作系统的角度来看,它被分配给应用程序。
  3. TestBigBlock现在失败了,因为虽然它会使用一个特殊的内存块,但是它与堆共享整个内存空间,并且在2完成之后就没有足够的内存空间了。

答案 3 :(得分:1)

C ++程序中有绝对没有内存泄漏。真正的罪魁祸首是内存碎片化。

为了确保(关于内存泄漏点),我在Valgrind上运行了这个程序,它没有在报告中提供任何内存泄漏信息。

//Valgrind Report
mantosh@mantosh4u:~/practice$ valgrind ./basic
==3227== HEAP SUMMARY:
==3227==     in use at exit: 0 bytes in 0 blocks
==3227==   total heap usage: 20,017 allocs, 20,017 frees, 4,021,989,744 bytes allocated
==3227== 
==3227== All heap blocks were freed -- no leaks are possible

请在原始问题中找到我对您的查询/疑问的回复。

  

罪魁祸首似乎是在TestMemory()之后的应用程序   虚拟内存保持在827125760(无论我多少次   叫它)。   是的,真正的罪魁祸首是在TestMemory()函数中完成的隐藏碎片。只是为了理解碎片,我从wikipedia获取了片段

&#34; 当空闲内存被分成小块并被分配的内存散布在一起时。当某些存储分配算法无法有效地命令程序使用的内存时,它是某些存储分配算法的弱点。结果是,尽管可以使用免费存储,但它实际上是无法使用的,因为它被分成几个太小而无法满足应用需求的部分。 例如,考虑一种情况,其中程序分配3个连续的存储器块然后释放中间块。内存分配器可以使用这个空闲的内存块来进行将来的分配。但是,如果要分配的内存大小超过此空闲块,则无法使用此块。&#34;

上面解释的段落很好地解释了内存碎片。一些分配模式(例如频繁分配和交易位置)会导致内存碎片,但其最终影响(.ie内存分配 1.5GB 因为不同的OS /堆管理器具有不同的策略和实现,因此在不同的系统上会有很大的不同。 例如,您的程序在我的机器(Linux)上运行得非常好,但是您遇到了内存分配失败。

关于您对VM大小的观察保持不变:任务管理器中看到的VM大小与我们的内存分配调用不成正比。它主要取决于承诺状态下的字节数。当您分配一些动态内存(使用new / malloc)并且您不在这些内存区域中编写/初始化任何内容时,它将不会进入提交状态,因此VM大小不会因此而受到影响。 VM大小取决于许多其他因素并且有点复杂,因此在理解程序的动态内存分配时我们不应完全依赖它。

  

因此,没有足够的免费VM regrion可以容纳1.5   GB。

是的,由于碎片,没有连续的 1.5GB 内存。应该注意的是,剩余(自由)内存总量将超过 1.5GB ,但不会处于分段状态。因此,没有大的连续记忆。

  

但我不确定为什么 - 因为我确实释放了我使用的内存。   它是一些&#34;性能优化&#34; CRT会尽量减少操作系统调用吗?

我已经解释了为什么它可能会发生,即使你已经释放了所有的记忆。现在,为了满足用户程序请求,OS将调用其虚拟内存管理器并尝试分配堆内存管理器将使用的内存。但抓住额外的内存确实取决于许多其他不易理解的复杂因素。

内存碎片的可能解决方案

我们应该尝试重用内存分配而不是频繁的内存分配/免费。可能存在一些模式(例如特定顺序的特定请求大小分配),这可能导致整体存储器进入分段状态。您的程序可能会有大量的设计更改,以改善内存碎片。这是一个复杂的主题,需要内部理解内存管理器才能理解这些事情的完整根本原因。

然而,基于Windows的系统上存在工具,我不太清楚。但是我找到了一篇关于哪个工具(在Windows上)可以用来理解和检查程序碎片状态的优秀SO帖子。

https://stackoverflow.com/a/1684521/2724703