std map和shared_ptr的奇怪内存行为

时间:2014-12-12 14:59:46

标签: linux c++11 memory-leaks 64-bit gcc4.7

下面的代码在我的Debian机器上引发了奇怪的内存行为。 即使在清除了地图之后,htop也显示该程序仍然使用大量内存,这让我觉得存在内存泄漏。奇怪的是,它只在某些情况下出现。

#include <map>
#include <iostream>
#include <memory>


int main(int argc, char** argv)
{
    if (argc != 2)
    {
        std::cout << "Usage: " << argv[0] << " <1|0> " << std::endl;
        std::cout << "1 to insert in the second map and see the problem "
                "and 0 to not insert" << std::endl;
        return 0;
    }

    bool insertion = atoi(argv[1]);
    std::map<uint64_t, std::shared_ptr<std::string> > mapStd;
    std::map<uint64_t, size_t> counterToSize;
    size_t dataSize = 1024*1024;
    uint64_t counter = 0;

    while(counter < 10000)
    {
        std::shared_ptr<std::string> stringPtr =
                std::make_shared<std::string>(dataSize, 'a');
        mapStd[counter] = stringPtr;

        if (insertion)
        {
            counterToSize[counter] = dataSize;
        }
        if (counter > 500)
        {
            mapStd.erase(mapStd.begin());
        }
        std::cout << "\rInserted chunk " << counter << std::flush;

        counter++;
    }

    std::cout << std::endl << "Press ENTER to delete the maps" << std::endl;
    char a;
    std::cin.get(a); // wait for ENTER to be pressed

    mapStd.clear();   // clear both maps
    counterToSize.clear();

    std::cout << "Press ENTER to exit the program" << std::endl;

    std::cin.get(a); // wait for ENTER to be pressed
    return 0;
}

解释

代码在堆栈上创建两个映射(但如果它们是在堆上创建的,则问题是相同的)。然后它将字符串的std :: shared_ptr插入到第一个映射中。每个字符串的大小为1MB。一旦插入了500个字符串,就会删除每个新插入的第一个字符串,这样地图使用的总内存总是等于500MB。当插入总共10000个字符串时,程序等待用户按ENTER。如果启动程序并将1作为第一个参数传递,则对于第一个映射中的每个插入,还会对第二个映射进行另一次插入。如果第一个参数为0,则不使用第二个参数。首次按下ENTER后,两个地图都将被清除。程序仍然运行并再次等待按下ENTER,然后退出。

以下是事实:

  • 在我的64位Debian 3.2.54-2上,按下ENTER后(因此清除了映射后),以及以1作为第一个参数启动程序时(因此插入第二个映射) ),htop表示该程序仍然使用500MB的内存!!如果以0作为第一个参数启动程序,则正确释放内存。

  • 本机使用g ++ 4.7.2和libstdc ++。so.6.0.17。我试过用g ++ 4.8.2和libstdc ++。so.6.0.18,同样的问题。

  • 我尝试过使用g ++ 4.9.2和libstdc ++。so.6.0.20的64位Fedora 21,同样的问题。
  • 我尝试过使用g ++ 4.8.2和libstdc ++。so.6.0.19的 32位 Ubuntu 14.04,问题没有出现!
  • 我尝试使用g ++ 4.7.2和libstdc ++。so.6.0.17的 32位 Debian 3.2.54-2,问题没有出现!
  • 我试过64位Windows,问题没有出现!
  • 在存在问题的机器上,如果您反转清除地图的行(因此,如果先清除uint64_t,size_t地图,问题就会消失!

有人对这一切有解释吗?

1 个答案:

答案 0 :(得分:3)

我建议查看here后跟here。基本上,libc malloc开始使用mmap for&#34; large&#34;分配(> 128k)和小分配的brk / freelists。一旦其中一个大分配是免费的,它就会尝试调整可能使用malloc的大小,但前提是大小小于max(在第一个链接上定义)。在32位的情况下,你的字符串远远超过最大值,因此它继续使用mmap / munmap进行大量分配,并且只将较小的映射节点分配放入使用sbrk从系统检索的内存中。这就是为什么你没有看到&#39;问题&#39;在32位系统上。

另一位是碎片之一,当空闲时尝试合并内存并将其返回给系统。默认情况下,free会将小块粘贴到空闲列表中,以便他们准备好进行下一个请求。如果堆顶部有足够大的块空闲,它将尝试将内存返回给系统see comment here。阈值为64K。

如果您传递1,您的分配模式可能会使counterToSize地图的某些元素靠近堆顶部,从而阻止它在上一个版本的最后一个版本中被释放字符串。 counterToSize地图内部各种对象的释放量不足以触发阈值。

如果您切换.clear()来电的顺序,您会发现内存已经发布,就像您期望的那样。此外,如果你要分配一大块内存并在清除后立即立即释放它,它就会触发释放。 (在这种情况下,大的数字只需要超过128个字节 - 用于触发快速容器的最大大小。(即那个大小的自由,并且分配的数量小于该大小,只需进入列表。

我希望这很清楚。基本上,这不是一个真正的问题。您已经映射了一些页面。你没有在它们上面使用任何东西,但可能释放它们的最后一个免费版本太小而无法触发该代码路径。下次你尝试分配一些东西时,它会从你已经拥有的内存中拉出来(你可以再次完成整个循环而不增加内存)。

哦,你可以手动调用malloc_trim()并强制它进行合并/清理,如果你真的需要那些页面。