malloc实现会将已释放的内存返回给系统吗?

70

我有一个长时间运行的应用程序,需要频繁的内存分配和释放。任何malloc实现是否会将释放的内存返回给系统?

在这方面,以下是每个库的行为:

  • ptmalloc 1、2(glibc默认)或3
  • dlmalloc
  • tcmalloc(谷歌线程malloc)
  • Solaris 10-11默认的malloc和mtmalloc
  • FreeBSD 8默认的malloc(jemalloc)
  • Hoard malloc

更新

如果我的应用程序在白天和晚上(例如)的内存消耗可能非常不同,我是否可以强制任何malloc返回已释放的内存到系统?

如果没有这样的返回,释放的内存将被交换出去并多次交换回来,但此类内存仅包含垃圾。


21
你认为 free() 可能是别人对你开的一个实用玩笑吗? - President James K. Polk
10
我不希望系统对“已释放”的内存使用交换空间。更加经济和快速的方法是将该内存返回给系统,永远不要尝试将其放到磁盘上。为什么我需要用垃圾填充交换空间呢? - osgx
7
对于嵌入式或其他不使用交换空间的无盘系统来说,这实际上非常重要。 - nategoose
35
+1 给 OP,但我希望我能给所有说交换空间会解决问题的批评者 -1。这种对交换空间和虚拟内存的粗心态度是现代 Linux 桌面系统像 Windows 一样花费一半时间在硬盘上磨叽的原因之一... - R.. GitHub STOP HELPING ICE
15
我非常厌恶第一条评论的态度——事实上,大多数malloc实现不会将内存释放给操作系统,而那些能够释放的也很难。我猜想free()实际上是对GregS开的一个玩笑,而不是对原帖作者的回应。 - Nakedible
显示剩余9条评论
8个回答

41

以下分析仅适用于基于ptmalloc2算法的glibc。

有一些选项似乎有助于将释放的内存返回给系统:

  1. mallopt()(定义在malloc.h中)提供了一个选项,可以使用参数选项M_TRIM_THRESHOLD设置修剪阈值值,它表示允许在数据段顶部的最小可用自由内存量(以字节为单位)。如果该数量低于此阈值,则glibc调用brk()将内存返还给内核。

    Linux中M_TRIM_THRESHOLD的默认值设置为128K,设置更小的值可能会节省空间。

    通过在环境变量MALLOC_TRIM_THRESHOLD_中设置修剪阈值值,可以实现相同的行为,而无需进行任何源更改。

    然而,使用M_TRIM_THRESHOLD运行的初步测试程序表明,即使malloc分配的内存确实返回到系统,但最初通过brk()请求的实际内存块的剩余部分(竞技场) tend to be retained.

  2. 可以通过调用malloc_trim(pad)(定义在malloc.h中)来修剪内存竞技场并将任何未使用的内存返还给系统。此函数调整数据段的大小,在其中留下至少pad字节,并在释放不到一页的字节时失败。段大小始终是一页的倍数,i386上的一页为4,096字节。

    使用malloc挂钩功能实现使用malloc_trim的free()的此修改行为的实现不需要对核心glibc库进行任何源代码更改。

  3. glibc的free实现中使用madvise()系统调用。


6
请查看M_MMAP_THRESHOLD,这是一个阈值,高于该阈值malloc()将使用mmap()获取内存。显然,如果您释放超过此大小的块,则会将内存返回给操作系统。 - UncleZeiv

19
大多数实现不会费力识别整个“块”(其大小适合操作系统的情况相对较少)已被释放并可以返回的那些情况,但当然有例外。例如,引用自维基百科页面中所述,OpenBSD 中的情况如下:
在调用 `free` 时,使用 `munmap` 从进程地址空间中释放内存并取消映射。该系统旨在利用作为 OpenBSD 的 `mmap` 系统调用的一部分实施的地址空间布局随机化和间隙页特性来提高安全性,并检测 use-after-free 错误。因为大内存分配在释放后完全未映射,因此进一步使用会导致分段错误并终止程序。
虽然大多数系统并非像 OpenBSD 一样注重安全性。
了解这一点后,在编写具有已知短暂大量内存需求的长时间运行系统时,我总是尝试使用 `fork` 进程:父进程仅等待来自子进程的结果[[通常在管道上]],子进程执行计算(包括内存分配),返回结果 [[在所述管道上]],然后终止。这样,我的长时间运行过程在偶尔需求内存的长时间间隔期间不会无用地占用内存。其他备选策略包括为此类特殊需求切换到自定义内存分配器(C++ 使其相当容易,尽管具有虚拟机底层的语言如 Java 和 Python 通常不这样做)。

我能在多线程应用程序中使用fork吗?所以我真的不能使用fork。 - osgx
2
@osgx:是的,只要您仅在多线程应用程序中使用它来执行新进程,就可以分叉。实际上,“...子进程只能执行异步信号安全操作,直到调用其中一个exec函数为止”。 - Zan Lynx
1
@Zan:你是从哪得到这个想法的?在多线程进程中使用“fork”是允许的,只要你不通过使用它来破坏自己同步对象的状态。pthread_atfork为您提供了避免这样做的工具。 - R.. GitHub STOP HELPING ICE
1
@R. POSIX。你的想法是从哪里得到的? - Zan Lynx

7

我在我的应用程序中遇到了类似的问题,经过一些调查,我发现由于某种原因,glibc在分配的对象很小时(在我的情况下小于120字节),不会将内存返回给系统。
看看这段代码:

#include <list>
#include <malloc.h>

template<size_t s> class x{char x[s];};

int main(int argc,char** argv){
    typedef x<100> X;

    std::list<X> lx;
    for(size_t i = 0; i < 500000;++i){
        lx.push_back(X());
    }

    lx.clear();
    malloc_stats();

    return 0;
}

程序输出:
Arena 0:
system bytes     =   64069632
in use bytes     =          0
Total (incl. mmap):
system bytes     =   64069632
in use bytes     =          0
max mmap regions =          0
max mmap bytes   =          0

大约64 MB未返回到系统。当我将typedef更改为:typedef x<110> X;时,程序输出如下:

Arena 0:
system bytes     =     135168
in use bytes     =          0
Total (incl. mmap):
system bytes     =     135168
in use bytes     =          0
max mmap regions =          0
max mmap bytes   =          0

几乎所有的内存都已被释放。我还注意到使用malloc_trim(0)在任何情况下都会将内存释放给系统。
在上述代码中添加malloc_trim后,输出结果如下:

Arena 0:
system bytes     =       4096
in use bytes     =          0
Total (incl. mmap):
system bytes     =       4096
in use bytes     =          0
max mmap regions =          0
max mmap bytes   =          0

自2008年起,glibc的ptmalloc malloc_trim会遍历所有内存,并使用MADV_DONTNEED将完全释放的对齐4k页面返回给操作系统:https://dev59.com/cl4c5IYBdhLWcg3wapzz#47061458。这在man页面http://man7.org/linux/man-pages/man3/malloc_trim.3.html中部分记录(自glibc 2.8或2.9起)。尝试使用tcmalloc或jemalloc,它们通常比glibc的ptmalloc更好地将已释放的内存返回给操作系统。 - osgx
这可能是由于快速分配桶的错误导致的:https://sourceware.org/bugzilla/show_bug.cgi?id=14827 - ZachB

6

我也遇到了和原帖作者一样的问题。目前看来,使用tcmalloc是可能的。我找到了两个解决方案:

  1. compile your program with tcmalloc linked, then launch it as :

    env TCMALLOC_RELEASE=100 ./my_pthread_soft
    

    the documentation mentions that

    Reasonable rates are in the range [0,10].

    but 10 doesn't seem enough for me (i.e I see no change).

  2. find somewhere in your code where it would be interesting to release all the freed memory, and then add this code:

    #include "google/malloc_extension_c.h" // C include
    #include "google/malloc_extension.h"   // C++ include
    
    /* ... */
    
    MallocExtension_ReleaseFreeMemory();
    
第二种解决方案在我的情况下非常有效;第一种解决方案很好,但并不成功,例如找到正确的数字比较复杂。

谢谢,听起来相当有趣。 - osgx
对于 tcmalloc 1.7,此页面 表示它不会将任何内存返回给系统。 - osgx

4

在你列出的这些软件中,只有Hoard能够将内存返回给系统......但它是否真正能够实现这一点,很大程度上取决于你程序的分配行为。


谢谢!你能说出其他可以将内存返回给系统的分配器吗? - osgx
2
实际上,glibc 似乎也会这样做,但默认阈值仅适用于128kB及以上的分配。OpenBSD 对所有分配都是基于 mmap 的,因此 free 函数几乎总是会返回内存。然而,这存在一个巨大的性能折衷;在许多情况下,基于 mmap 的内存速度要慢得多,并且会引起很多页面错误以将其清零,这可能比它节省的少量交换压力更糟糕。 - Andrew McGregor
1
是的,但OpenBSD的动机是安全性,而不是性能(正如我的回答所提到的)。不知道glibc,我会调查一下,谢谢。 - Alex Martelli
我现在的想法是,ptmalloc和大多数基于ptmalloc和dlmalloc的程序将通过munmap和sbrk(-xxxx)向系统返回内存。 - osgx

3

对于所有“普通”的malloc,包括您提到的那些,内存被释放以供您的进程重用,但不会返回到整个系统。只有当您的进程最终终止时才会返回到整个系统。


5
正如下面有些人提到的,当malloc实现尝试将内存返回给操作系统时,会有特殊情况。我通常不会依赖这一点。相反,考虑在隔夜处理时使用mmap()映射一个新段,然后在完成后取消映射。显然,您需要考虑堆管理的问题,但是分配可以是非常简单的池式分配(即free是无操作),因为您将在隔夜处理作业结束时释放整个内存段。 - dkantowitz

3
简短回答:如果想要强制 malloc 子系统将内存返回给操作系统,请使用 malloc_trim()。否则,内存返回的行为取决于实现。
(Note: I have retained the HTML tags as requested)

5
malloc_trim() 只在 Linux/glibc 环境下可用。 - maxschlepzig

1

FreeBSD 12的malloc(3)使用jemalloc 5.1,该版本可以通过madvise(...MADV_FREE)将释放的内存(“脏页”)返回给操作系统。

释放的内存只有在由opt.dirty_decay_msopt.muzzy_decay_ms控制的时间延迟后才会返回;有关更多详细信息,请参见手册页面和此实现基于衰减的未使用脏页清除的问题

早期版本的FreeBSD附带了旧版的jemalloc,它也会返回已释放的内存,但使用不同的算法来决定何时清除哪些内存。


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