linux-带有std map和shared_ptr的奇怪内存行为

下面的代码在我的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,同样的问题.
>我已经在32位Ubuntu 14.04上使用g 4.8.2和libstdc .so.6.0.19进行了尝试,但问题没有出现!
>我已经在32位Debian 3.2.54-2上尝试了g 4.7.2和libstdc .so.6.0.17,问题没有出现!
>我已经在64位Windows上尝试过,但没有出现问题!
>在存在问题的计算机上,如果将要清除映射的行反转(因此如果先清除uint64_t,size_t映射,则问题将消失!

有人对此有一个解释吗?

解决方法:

我建议先看here,再看here.基本上,libc malloc首先使用mmap进行“大”分配(> 128k),使用brk / freelists进行小分配.一旦释放了一个较大的分配,它就会尝试在可能使用malloc的位置调整大小,但前提是该大小小于最大值(在第一个链接中定义).在32位的情况下,您的字符串远高于最大值,因此它继续对大型分配使用mmap / munmap,并且仅将较小的映射节点分配放入使用sbrk从系统检索的内存中.这就是为什么在32位系统上看不到“问题”的原因.

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

如果您传递1,则您的分配模式可能会使counterToSize映射的某些元素靠近堆的顶部,从而防止它在字符串的最后一次释放时被释放. counterToSize映射内的各种对象的释放量不足以触发阈值.

如果切换.clear()调用的顺序,则会发现内存已按预期释放.另外,如果您要分配大量内存并在清除后立即释放它,则会触发释放. (在这种情况下,大只需要超过128个字节-用于触发快速bin的最大大小.(即,具有该大小的free且分配的空间小于该大小的free进入列表.

我希望这很清楚.基本上,这不是真正的问题.您已经映射了一些页面.您没有在它们上使用任何东西,但是可能释放它们的最后一个免费软件太小,无法触发该代码路径.下次尝试分配某些内容时,它将从您已有的内存中提取(您可以在不增加内存的情况下再次执行整个循环).

哦,如果您当时确实需要返回页面,则可以手动调用malloc_trim()并强制它进行合并/清除.

上一篇:C11中的通用字符名(Universal Character Names)


下一篇:C语言 C99标准与C11标准的部分更改的函数对比