使用malloc进行页面分配时存在内存泄漏问题

8

考虑下面这段创建了100,000个大小为4KB的页面的C代码,然后释放99,999个页面,并最终释放最后一个页面:

#include <stdio.h>
#include <stdlib.h>

#define NUM_PAGES 100000

int main() {
    void *pages[NUM_PAGES];

    int i;
    for(i=0; i<NUM_PAGES; i++) {
        pages[i] = malloc(4096);
    }

    printf("%d pages allocated.\n", NUM_PAGES);
    getchar();

    for(i=0; i<NUM_PAGES-1; i++) {
        free(pages[i]);
    }

    printf("%d pages freed.\n", NUM_PAGES-1);
    getchar();

    free(pages[NUM_PAGES-1]);

    printf("Last page freed.\n");
    getchar();

    return 0;
}

如果你编译并运行它,同时监控进程的内存使用情况,你会发现在第一个getchar之前(当内存被分配给100,000个页面时),内存使用量达到了约400MB,然后即使在99,999个页面被释放之后(第二个getchar之后),内存使用量仍然保持不变,最后当最后一页被释放时,内存使用量降至1MB。
那么,我的问题是为什么会出现这种情况?为什么只有当所有页面都被释放时才将整个内存返回给操作系统?是否有任何页面大小或页面对齐方式可以防止这种情况发生?我的意思是,是否有任何页面大小或对齐方式可以使任何已分配的页面在只释放一个页面时就完全返回给操作系统?

3
这是因为分配的空间太小,您的C库使用sbrk()来动态调整其未初始化的数据。由于它是顺序的,因此只能在释放最新的分配时缩小。如果将分配大小增加到131072字节(128k),则strace显示它会改用mmap()进行分配,并且每个free()实际上会将分配返回给操作系统。因此,请使用分配缓存,并仅向操作系统请求/返回较大的块。 - Nominal Animal
@NominalAnimal,你救了我的命!非常感谢您上面和下面的评论。那正是我从答案中期望的。如果您直接回答(而不是评论),我会接受您的答案... - LuisABOL
1个回答

5
这完全取决于实现方式,但我认为这与内存分配器的工作方式有关。通常,当内存管理器需要从操作系统获取更多内存时,它会调用sbrk函数请求额外的内存。该函数的典型实现是,操作系统存储一个指向下一个空闲地址的指针,进程可以在其中获取空间。内存增长类似于堆栈,就像调用堆栈的工作方式一样。例如,如果您分配了五个页面的内存,它可能看起来像这样:
 (existing memory) | Page 0 | Page 1 | Page 2 | Page 3 | Page 4 | (next free spot)

通过这种设置,如果您释放页面0-4,则程序内部的内存管理器将标记它们为空闲状态,如下所示:

 (existing memory) |                                   | Page 4 | (next free spot)

由于操作系统以类似堆栈的方式分配内存,直到页面4不再使用,它才能从程序中回收所有这些内存。一旦您释放了最后一页,进程的内存将如下所示:

 (existing memory) |                                              (next free spot)

此时,程序的内存管理器可以将大量的可用空间返回给操作系统:

 (existing memory) | (next free spot)

换句话说,由于内存分配为堆栈形式,直到您释放了最后一个分配的内容之前,操作系统无法回收任何内存。
希望对您有所帮助!

好的,非常感谢您的回答!关于内存分配的解释非常好。但是,我想知道是否有任何方法可以避免类似堆栈的内存分配,我的意思是分配单独的页面并逐个取消分配。 - LuisABOL
2
@LuisAntonioBotelhoO.Leite 你可能需要使用操作系统相关的调用来直接从操作系统获取/释放内存。在Windows下,您可以使用VirtualAlloc和VirtualFree。在Unix上,请使用sbrk(2)。请注意,建议您仅使用malloc/free并避免使用sbrk。 - Anthony
4
在Linux上,较大的内存分配使用mmap()而不是sbrk()。当使用free()释放内存映射分配时,它们会立即返回给操作系统。在我的Ubuntu 12.04.2 LTS x86-64上,使用嵌入式GNU C库2.15-0ubuntu14和6GB RAM,131072字节或更多的分配将使用mmap()。因此,建议使用sbrk()是完全错误的。更好的方法是将分配合并为较大的单元。顺便说一下,这种方法在所有操作系统上都应该可以正常工作。 - Nominal Animal
3
我建议使用mmap而不是sbrk - Basile Starynkevitch
@NominalAnimal- 对不起...我并不是想建议使用sbrk作为一个好的实现方式。我原本以为这可能是malloc的工作方式,并打算利用这种理解来解释发生了什么。在这里,mmap绝对是更好的选择。 - templatetypedef
@templatetypedef:不用担心!malloc()的实现确实有所不同,我甚至很惊讶即使GNU C库仍然使用sbrk()。匿名私有的mmap()页面是便宜且易于实现的,因此如果您需要特殊的分配器(基于slab、基于池等),请放心去做吧! - Nominal Animal

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