5

在我们的一个项目中调查内存链接时,我遇到了一个奇怪的问题。不知何故,当父容器超出范围并且除了小对象之外不能使用时,分配给对象的内存(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(不管我调用多少次)。因此,没有足够大的免费 VM regrion 来容纳 1.5 GB。但我不确定为什么——因为我肯定会释放我使用的内存。是否有一些“性能优化”CRT 可以最大限度地减少操作系统调用?

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

4

4 回答 4

3

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

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

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

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

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

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

于 2014-04-10T13:37:08.230 回答
2

您的 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() 函数期间完成的隐藏碎片。为了理解碎片,我从维基百科中获取了片段

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

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

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

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

是的,由于碎片,没有连续的1.5GB内存。需要注意的是,总剩余(空闲)内存将超过1.5GB,但不会处于碎片状态。因此没有大的连续内存。

但我不确定为什么——因为我肯定会释放我使用的内存。是否有一些“性能优化”CRT 可以最大限度地减少操作系统调用?

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

内存碎片的可能解决方案

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

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

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

于 2014-04-09T18:55:15.027 回答
1

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

于 2013-10-29T09:13:02.293 回答
1

这里没有任何迹象表明真正的“泄漏”。你描述的记忆模式并不出人意料。这里有几点可能有助于理解。发生的事情高度依赖于操作系统。

  • 一个程序通常有一个可以扩展或缩小长度的堆。然而,它是一个连续的内存区域,所以改变大小只是改变堆末端的位置。这使得将内存“归还”给操作系统变得非常困难,因为即使是那个空间中的一个很小的对象也会阻止它的缩小。在 Linux 上,您可以查找函数“brk”(我知道您在 Windows 上,但我认为它有类似的功能)。

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

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

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

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


例如,在提供的代码中:

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