Linux 分配器不会释放小内存块。

17

Linux的glibc分配器似乎表现得有些奇怪,希望有人能解释一下。这是我拥有的源文件:

first.cpp:

#include <unistd.h>
#include <stdlib.h>
#include <list>
#include <vector>

int main() {

  std::list<char*> ptrs;
  for(size_t i = 0; i < 50000; ++i) {
    ptrs.push_back( new char[1024] );
  }
  for(size_t i = 0; i < 50000; ++i) {
    delete[] ptrs.back();
    ptrs.pop_back();
  }

  ptrs.clear();

  sleep(100);

  return 0;
}

second.cpp:

#include <unistd.h>
#include <stdlib.h>
#include <list>

int main() {

  char** ptrs = new char*[50000];
  for(size_t i = 0; i < 50000; ++i) {
    ptrs[i] = new char[1024];
  }
  for(size_t i = 0; i < 50000; ++i) {
    delete[] ptrs[i];
  }
  delete[] ptrs;

  sleep(100);

  return 0;
}

我同时编译:

$ g++ -o first first.cpp
$ g++ -o second second.cpp

我运行first,在它睡眠时,我查看它的驻留内存大小:

当我编译first.cpp并运行它时,我使用ps查看内存:

$ ./first&
$ ps aux | grep first
davidw    9393  1.3  0.3  64344 53016 pts/4    S    23:37   0:00 ./first


$ ./second&
$ ps aux | grep second
davidw    9404  1.0  0.0  12068  1024 pts/4    S    23:38   0:00 ./second

请注意驻留内存大小。第一个程序中,驻留内存大小为53016k;而在第二个程序中,它是1024k。由于某种原因,第一个程序从未将分配的内存释放回内核。

为什么第一个程序不会将内存释放回内核,而第二个程序会?我知道第一个程序使用了链表,链表可能在和我们要释放的数据相同的页面上分配了一些节点。但是,当我们弹出这些节点并清除链表时,那些节点应该会被释放掉。如果你通过valgrind运行这两个程序,它们都不会出现内存泄漏。可能发生的情况是,第一个程序中的内存被碎片化了,而第二个程序中没有。然而,如果页面上的所有内存都被释放了,那么如何才能将该页面释放回内核呢?内存何时才能被释放回内核?我该如何修改first.cpp(继续将char*放在列表中),以便将内存释放回内核。


2
使用收缩以适应功能,可以在此处找到描述:https://dev59.com/kVbUa4cB1Zd3GeqPA7P2。在这种情况下,执行 std::list<char*>().swap(ptrs) - jxh
1
恐怕这里还有其他问题... 这是我的新程序:int main() { { std::list ptrs; for(size_t i = 0; i < 50000; ++i) { ptrs.push_back( new char[1024] ); } for(size_t i = 0; i < 50000; ++i) { delete[] ptrs.back(); ptrs.pop_back(); } ptrs.clear(); std::list().swap(ptrs); } sleep(100); return 0; }运行ps命令的结果相同:davidw 9961 0.0 0.3 64344 53016 pts/4 S 00:31 0:00 ./first - user1418199
它被标记为C,因为在使用malloc/free时,你会在C中遇到相同的问题。我认为将来编写C程序的人可能会发现这很有用。 - user1418199
1
你是否验证了你的第二个程序实际上分配了内存?我最近记得读到过有关优化掉没有中间代码使用结果的malloc/free对的文章,同样的逻辑也适用于new/delete对。 - user743382
1
它不应该,至少没有进行全面的程序分析。在 C++ 中,对 operator new 和 operator delete 的调用是可观察行为。 - James Kanze
显示剩余10条评论
4个回答

18

这种行为是有意的,glibc使用可调节的阈值来决定是否实际将内存返回给系统,还是将其缓存以供稍后重用。在您的第一个程序中,每个push_back都会进行大量小型分配,这些小型分配不是连续的块,并且可能低于阈值,因此不会返回给操作系统。

在清除列表后调用malloc_trim(0)应该会导致 glibc 立即将自由内存的顶部区域返回给系统(下一次需要内存时需要一个sbrk系统调用)。

如果您真的需要覆盖默认行为(我不建议除非分析表明它确实有所帮助),那么您应该使用 strace 和/或尝试使用mallinfo来查看程序实际发生了什么,可能使用mallopt来调整返回内存到系统的阈值。


1
关于malloc_trim函数:自glibc 2.8版本起,此函数在所有区域和所有带有整个空闲页面的块中释放内存。在glibc 2.8之前,此函数只在主区域堆顶释放内存。(参考:http://man7.org/linux/man-pages/man3/malloc_trim.3.html) - toddwz

5

它会保留较小的块,以便在您再次请求它们时可用。这是一种简单的缓存优化,不需要特别关注。


3

通常情况下,new 分配的内存只有在进程终止时才会返回给系统。在第二种情况下,我怀疑 libc 使用了一个特殊的分配器来处理非常大的连续块,该分配器会将其返回,但是我非常惊讶如果你的 new char[1024] 中的任何一个被返回,而且在许多 Unix 上,即使是大块也不会被返回。


2

(编辑我的答案,因为这里真的没有问题。)

正如已经指出的那样,这里实际上并不存在任何问题。 Johnathon Wakely说得很对。

当Linux上的内存利用率不符合我的预期时,我通常会使用工具,并分析/proc/self/maps文件来开始我的分析。

mtrace通过在两个调用之间添加括号来使用,一个用于启动跟踪,另一个用于结束跟踪。

  mtrace();
  {
      // do stuff
  }
  muntrace();

mtrace 调用仅在设置了 MALLOC_TRACE 环境变量时才能生效。它指定了 mtrace 日志输出的文件名。该日志输出可以用于分析内存泄漏。一个叫做 mtrace 的命令行程序可用于分析输出结果。

$ MALLOC_TRACE=mtrace.log ./a.out
$ mtrace ./a.out mtrace.log

/proc/self/maps文件提供了当前程序中正在使用的内存映射区域列表,包括匿名区域。它可以帮助识别特别大的区域,然后需要进行额外的调查以确定该区域与什么相关联。下面是一个简单的程序,将/proc/self/maps文件转储到另一个文件中。

void dump_maps (const char *outfilename) {
  std::ifstream inmaps("/proc/self/maps");
  std::ofstream outf(outfilename, std::ios::out|std::ios::trunc);
  outf << inmaps.rdbuf();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接