【问题标题】:Strange memory behavior with std map and shared_ptrstd map 和 shared_ptr 的奇怪内存行为
【发布时间】:2014-12-12 14:59:46
【问题描述】:

下面的代码在我的 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,然后退出。

事实如下:

    1234563 ), htop 表示程序 STILL USE 500MB OF MEMORY!!如果程序以 0 作为第一个参数启动,则内存被正确释放。
  • 本机使用 g++4.7.2 和 libstdc++.so.6.0.17。我试过 g++4.8.2 和 libstdc++.so.6.0.18,同样的问题。

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

有人对这一切有解释吗???

【问题讨论】:

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


【解决方案1】:

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

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

在传递1 的情况下,您的分配模式可能会在堆顶部附近留下counterToSize 映射的某些元素,从而阻止它被最后一个字符串释放。 counterToSize 映射内的各种对象的释放量不足以触发阈值。

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

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

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

【讨论】:

  • malloc_trim 有效,感谢您的提示。但是在清除后立即分配块不会触发释放。我尝试过从 64 字节到 10 MB 的不同大小...
  • 当我添加 int64_t* foo = new int64_t[16]; delete[] foo; 时,它最终是空的。小于 16 的大小不会触发,因为分配直接进入 fastbin 并跳过所有其他逻辑。如果它大于这个值,它将与附近的块合并,然后在将其插入空闲列表并触发清理之前检查大小。如果你只是分配了内存,你并没有触发释放逻辑。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-10-24
  • 1970-01-01
  • 2013-05-19
  • 1970-01-01
  • 1970-01-01
  • 2013-01-13
  • 2011-06-19
相关资源
最近更新 更多