为什么在分配/解除分配许多小对象后内存不可重用



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

最小的例子:当程序启动时,我可以毫无问题地分配一个 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(无论我调用它的次数)。因此,没有足够大的免费虚拟机来容纳 1.5 GB。但我不确定为什么 - 因为我肯定会释放我使用的内存。CRT 是否为最大限度地减少操作系统调用而进行的一些"性能优化"?

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

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

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

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

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

解决方法是使用 Windows 堆函数(HeapCreate() 等)创建一个私有堆,通过该堆分配内存(根据需要将自定义C++分配器传递给容器类),然后在想要内存时销毁该堆 - 这也有一个令人高兴的副作用,即非常快,而不是在循环中处理无数个块。

"内存中剩余的内容"首先会导致问题:内存本身没有剩余任何内容,更多的是释放的块被标记为空闲但未合并的情况。堆管理器有一个地址空间的表/映射,它不允许你分配任何会迫使它把可用空间合并到一个连续块中的东西(大概是性能启发式)。

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(无论 I 的次数如何 叫它)。 是的,真正的罪魁祸首是在 TestMemory() 函数期间完成的隐藏碎片。只是为了理解碎片化,我从维基百科中获取了片段

"当可用内存被分成小块并穿插在分配的内存中时。这是某些存储分配算法的弱点,当它们无法有效地对程序使用的内存进行排序时。结果是,尽管有免费存储可用,但它实际上是无法使用的,因为它被分成太小的部分,单独无法满足应用程序的需求。例如,考虑这样一种情况:程序分配 3 个连续的内存块,然后释放中间的块。内存分配器可以使用此空闲内存块进行将来的分配。但是,如果要分配的内存大小大于此可用块,则它不能使用此块。

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

关于您对 VM 大小的

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

因此,没有足够大的免费虚拟机来容纳 1.5 国标。

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

但我不确定为什么 - 因为我肯定会释放我使用的内存。 CRT 是否为最大限度地减少操作系统调用而进行的一些"性能优化"?

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

内存碎片的可能解决方案

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

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

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

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

这里没有任何内容表明真正的"泄漏"。你描述的记忆模式并不意外。这里有几点可能有助于理解。发生的情况高度依赖于操作系统。

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

  • 大额分配通常采用不同的策略完成。不是将它们放在通用堆中,而是创建一个额外的内存块。当它被删除时,这个内存实际上可以"返回"给操作系统,因为它保证没有任何东西在使用它。

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

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

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


例如,在提供的代码中:

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

最新更新