malloc函数可以分配的最大内存是多少?

62

我想知道在我这台机器上(1GB RAM,160GB HD,Windows平台),可以使用malloc一次性分配多少内存。

我了解到,malloc可以分配的最大内存受物理内存(在堆上)的限制。

此外,当程序占用内存超过一定程度时,电脑会停止工作,因为其他应用程序没有获得它们所需的足够内存。

所以为了确认,我写了一个简单的C程序:

int main(){  
    int *p;
    while(1){
        p=(int *)malloc(4);
        if(!p)break;
    }   
}

我原本希望在内存分配失败时循环会停下来,但我的电脑由于进入了一个无限循环而卡住了。

我等了大约一个小时,最后不得不强制关闭电脑。

一些问题:

  • malloc 是否也会从硬盘中分配内存?
  • 上述行为的原因是什么?
  • 为什么循环没有在任何时间点中断?
  • 为什么没有出现任何内存分配失败的情况?

3
为什么要使用malloc(4),而不是malloc(4096)、malloc(8192)或其他大小呢?无论如何。 - Andrei Ciobanu
1
当然,它可以是 int 大小的任何倍数。不是吗? - Vikas
1
不要在C语言中将malloc的结果强制转换。 - phuclv
1
不,使用malloc(1ULL<<30)这样的大型分配和许多小型分配之间存在非常显著的区别。在第一种方式中,您将在用完RAM+交换空间以存储簿记信息之前耗尽虚拟地址空间,而不是在使用所有物理RAM时进行交换抖动。当您的分配为多个页面时,即使malloc在每个分配的开头存储簿记信息,大多数页面也不会被触及。而微小的分配使用更多的空间进行簿记和对齐,而不是实际的4字节分配,因此如果您正在计算总分配大小,则会有巨大的开销。 - Peter Cordes
10个回答

59

我读到 malloc 可以分配的最大内存受物理内存(在堆上)的限制。

错误:大多数计算机/操作系统都支持由磁盘空间支持的虚拟内存。

有些问题: malloc 是否也会从硬盘中分配内存?

malloc 向操作系统请求内存,操作系统可能使用一些磁盘空间。

以上行为的原因是什么?为什么循环没有在任何时候中断?

为什么没有出现内存分配失败?

你每次只要求得太少了:循环最终会结束(当你的机器因虚拟内存远远超过物理内存,导致频繁进行磁盘访问,并出现所谓的“抖动”现象而变得极其缓慢后),但这之前已经让你不耐烦了。可以尝试每次获取1兆字节。

当程序超出一定程度的内存消耗时,计算机停止工作,因为其他应用程序没有获得所需的足够内存。

完全停止不太可能,但当通常只需几微秒的操作最终需要(例如)几十毫秒来完成时,这四个数量级肯定会让计算机感觉基本上停滞了,原本只需要一分钟的事情可能要花费一周的时间。


你的内存大小为1GB,并不意味着malloc会一直使用那么多。这实际上取决于操作系统分配给进程的内存量。在这种情况下,根据代码,分配给进程的内存将非常低。然后它会继续在虚拟内存上分配内存。 - Laz
4
实际上,在某些平台上,即使请求的内存大小超过了RAM + 交换空间的大小,malloc函数仍可能成功。例如,在Linux上请求内存意味着映射/dev/zero文件,这又意味着只是将页面标记为零 - 除非更改内容,否则不需要消耗太多的内存或交换空间。 - skyking
但是如果请求的大小超过地址空间(在32位系统上大约为4 GB; 在大多数x86_64系统上大约为16 TB),它将失败。如果您将某些内容放入内存中,它将必须分配实际内存,但如果您的操作系统支持压缩内存,则可以进行压缩。 - JustinCB

28

我知道这个帖子很旧了,但是对于任何想要自己尝试的人,可以使用这段代码

#include <stdlib.h>

int main() {
int *p;
while(1) {
    int inc=1024*1024*sizeof(char);
    p=(int*) calloc(1,inc);
    if(!p) break;
    }
}

运行

$ gcc memtest.c
$ ./a.out

运行这段代码后,它会一直占用内存直到被内核强制终止。为了避免“惰性计算”,需要使用calloc而不是malloc。这个想法来自于这个主题:Malloc Memory Questions

这段代码很快就填满了我的RAM(4GB),然后在大约2分钟内填满了我的20GB交换分区,最后崩溃了。当然,我的操作系统是64位的Linux。


2
我刚刚在一台拥有192Gb内存/4Gb交换空间的机器上尝试了相同的程序。在一分钟内,它消耗了高达175Gb的内存,然后慢慢地填满了交换空间。当只剩下24kb的交换空间时,程序被终止了。 - Sebastian
1
你所谓的“惰性求值”可能允许内核为每个分配但未写入的页面使用零页面。压缩(特别是用于交换)甚至去重(如某些虚拟化程序目前所做的那样)可以减少实际所需的内存。当然,malloc具有存储开销,页表会增加开销,程序具有非堆内存,操作系统使用内存等。 - user2467198
2
一个好的calloc(3)实现不会在从mmap(2)获得页面后再次访问它们,因为它们已经被清零了。实际上,这最终会触发OOM killer的原因是malloc的额外的书记信息使用了内存。如果你用strace跟踪它,你会看到mmap(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f4fc4d14000。分配大小为1052672,即1MiB + 4096,而那额外的页面可能就是glibc的malloc实际上脏化的。例如,在我的具有5GiB物理内存的桌面上,我可以无需磁盘活动就使用calloc分配16GiB(以1MiB块的形式)。 - Peter Cordes
未被触及的虚拟页面仍然映射到相同的物理清零页面。 - Peter Cordes

14

/proc/sys/vm/overcommit_memory 控制 Linux 上的最大内存分配。

例如,在 Ubuntu 19.04 上,我们可以很容易地使用 strace 看到 malloc 是通过使用 mmap(MAP_ANONYMOUS 实现的。

然后,运行 man proc 命令,描述了 /proc/sys/vm/overcommit_memory 如何控制最大分配量:

这个文件包含了内核虚拟内存计数模式。取值为:

  • 0:启用启发式内存超限(默认情况)
  • 1:总是超限,不进行检查
  • 2:总是检查,不超限

在模式 0 中,对于使用 MAP_NORESERVE 的 mmap(2) 调用不会检查,而且默认的检查非常弱,存在进程“OOM-killed”的风险。

在模式 1 中,内核假装始终有足够的内存,直到实际内存耗尽。这种模式的一种用途是使用大型稀疏数组的科学计算应用程序。在 Linux 内核版本 2.6.0 之前的任何非零值都会暗示模式 1。

在模式 2 中(自 Linux 2.6 起可用),可以分配的总虚拟地址空间(/proc/meminfo 中的 CommitLimit)的计算公式为:

CommitLimit = (total_RAM - total_huge_TLB) * overcommit_ratio / 100 + total_swap

其中:

  • total_RAM是系统上的RAM总量;
  • total_huge_TLB是为巨大页面分配的内存大小;
  • overcommit_ratio是/proc/sys/vm/overcommit_ratio中的值;以及
  • total_swap是交换空间的大小。

例如,在物理RAM为16GB,交换空间为16GB,没有专门用于巨大页面的空间,并且overcommit_ratio为50的系统上,此公式会产生24GB的CommitLimit。

自Linux 3.14以来,如果/proc/sys/vm/overcommit_kbytes中的值非零,则CommitLimit将计算为:

CommitLimit = overcommit_kbytes + total_swap

请参见/proc/sys/vm/admiin_reserve_kbytes和/proc/sys/vm/user_reserve_kbytes的描述。

在5.2.1内核树中,Documentation/vm/overcommit-accounting.rst也提供了一些信息,尽管略少:

Linux内核支持以下过度承诺处理模式

  • 0 启发式的过度承诺处理。地址空间的明显过度承诺被拒绝。用于典型系统。它确保一个严重的野外分配失败,同时允许过度承诺减少交换使用。在此模式下,root被允许稍微分配更多的内存。这是默认值。

  • 1 总是过度承诺。适用于某些科学应用程序。经典例子是使用稀疏数组的代码,并依赖于虚拟内存几乎完全由零页面组成。

  • 2 不要过度承诺。不允许系统的总地址空间提交超过交换+可配置数量(默认为物理RAM的50%)。根据您使用的数量,在大多数情况下,这意味着在访问页面时不会杀死进程,但会适当地接收有关内存分配的错误。

    适用于希望保证其内存分配将来可用而无需初始化每个页面的应用程序。

最小实验

我们可以轻松看到允许的最大值:

main.c

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char **argv) {
    char *chars;
    size_t nbytes;

    /* Decide how many ints to allocate. */
    if (argc < 2) {
        nbytes = 2;
    } else {
        nbytes = strtoull(argv[1], NULL, 0);
    }

    /* Allocate the bytes. */
    chars = mmap(
        NULL,
        nbytes,
        PROT_READ | PROT_WRITE,
        MAP_SHARED | MAP_ANONYMOUS,
        -1,
        0
    );

    /* This can happen for example if we ask for too much memory. */
    if (chars == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }

    /* Free the allocated memory. */
    munmap(chars, nbytes);

    return EXIT_SUCCESS;
}

GitHub上游

编译并运行以分配1GiB和1TiB:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
./main.out 0x40000000
./main.out 0x10000000000

我们可以尝试调整分配值来查看系统允许的情况。

我找不到关于 0(默认值)的精确文档,但在我的32GiB RAM机器上,它不允许1TiB的分配:

mmap: Cannot allocate memory

如果我启用无限制的过度承诺:

echo 1 | sudo tee /proc/sys/vm/overcommit_memory

那么,如果分配1 TiB的空间,就可以正常工作。

模式2有很好的文档说明,但我懒得进行精确计算来验证它。但是我只想指出,在实践中,我们被允许分配大约:

overcommit_ratio / 100

总共RAM的大小,overcommit_ratio默认为50,因此我们可以分配大约一半的总RAM。

VSZ和RSS以及内存不足杀手

到目前为止,我们只是分配了虚拟内存。

然而,在某个时刻,如果你使用了足够多的这些页面,Linux将不得不开始杀死一些进程。

我在这里详细说明了这一点:Linux内存管理中的RSS和VSZ是什么


9

试试这个

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

main() {
    int Mb = 0;
    while (malloc(1<<20)) ++Mb;
    printf("Allocated %d Mb total\n", Mb);
}

为此,需要包含stdlib和stdio头文件。
这段文字摘自《深入C语言秘籍》。


糟糕...如果要交换100GB的数据,你需要等待相当长的时间才能得到结果。最好在此期间不要运行其他计算机程序! - Alexis Wilke
3
在Linux系统中,默认虚拟内存设置下,你的程序最终会被杀死(使用SIGKILL信号),而不是让malloc函数实际返回NULL。 - Peter Cordes
2
就像其他人指出的那样,这不会按预期工作。(我猜有人需要写《深入深入C秘密》)。它将被杀死而不是返回空指针。它还可能使用交换或甚至基于磁盘的空间,具体取决于您的系统。如果您的系统使用内存过度承诺以及分配的内存的惰性评估,那么在触发由于内存不足而杀死进程的操作系统逻辑之前,它很容易看起来支持数万个千兆字节等。 - ely

7

malloc进行自己的内存管理,管理小的内存块,但最终使用Win32 堆函数来分配内存。您可以将malloc视为“内存转售商”。

Windows内存子系统包括物理内存(RAM)和虚拟内存(硬盘)。当物理内存变得稀缺时,一些页面可以从物理内存复制到硬盘上的虚拟内存中。Windows会透明地执行此操作。

默认情况下,启用虚拟内存并占用硬盘上的可用空间。因此,您的测试将继续运行,直到它分配了进程的全部虚拟内存(在32位Windows上为2GB)或填满了硬盘。


4
根据C90标准,您可以获得至少一个32 kBytes大小的对象,这可能是静态、动态或自动内存。C99保证至少64 kBytes。对于任何更高的限制,请参阅编译器的文档。
此外,malloc的参数是size_t类型,该类型的范围为[0,SIZE_MAX],因此您可以请求的最大值是SIZE_MAX,该值因实现而异,并在<limits.h>中定义。

3
我其实不知道为什么会失败,但需要注意的一点是`malloc(4)`可能并不能真正给你4个字节,所以这种技术并不是一种准确找到最大堆大小的方法。
我从我的问题here中发现了这一点。
例如,当您声明4个字节的内存时,您的内存直接前面的空间可能包含整数4,作为向内核表示您请求了多少内存的指示。

实际上,malloc通常会返回16字节的倍数。原因有两个。一是标准规定malloc应该返回与任何数据对齐兼容的指针。因此,不能返回相距不到16字节的地址。另一个原因是释放的块通常存储用于内部内存管理的一些数据,而太短的块 - 比如4字节 - 无法存储它。 - kriss
[i] 空闲块通常存储用于内存管理的一些数据,而太短的块(比如只有4个字节)无法存储它。[/i]您能否提及是哪种类型的数据? - Vikas

1
malloc函数是否也从硬盘上分配内存?
实现malloc()函数取决于libc的实现和操作系统(OS)。通常情况下,malloc()函数并不总是向操作系统请求RAM,而是返回一个指针,该指针指向以前由libc“拥有”的已分配内存块。
在POSIX兼容系统中,这个由libc控制的内存区域通常使用系统调用brk()来增加。这不允许在两个仍存在的分配之间释放任何内存,这会导致在顺序分配区域A、B、C并释放B后,进程看起来仍然使用所有RAM。这是因为围绕区域B的A和C区域仍在使用,因此无法返回从操作系统分配的内存。
许多现代的malloc()实现都具有某种启发式算法,其中小的分配使用通过brk()保留的内存区域,而“大”的分配则使用匿名虚拟内存块,通过mmap()使用MAP_ANONYMOUS标志进行保留。这允许在稍后调用free()时立即返回这些大的分配。通常,mmap()的运行时性能略低于使用先前保留的内存,这就是malloc()实现这种启发式算法的原因。
brk()和mmap()都从操作系统中分配虚拟内存。虚拟内存始终可以由交换支持,交换可能存储在操作系统支持的任何存储器中,包括硬盘。
如果您运行Windows,则系统调用具有不同的名称,但基本行为可能是相同的。
上述行为的原因是什么?
由于您的示例代码从未触及内存,我猜测您正在看到操作系统实现了虚拟RAM的写时复制,并且该内存映射到默认情况下填充整个页面的共享页面。现代操作系统这样做是因为许多程序分配的RAM比它们实际需要的更多,并且默认情况下为所有内存分配使用共享零页避免需要为这些分配使用真实的RAM。
如果您想测试操作系统如何处理您的循环并实际保留真正的存储空间,则需要向您分配的内存中写入一些内容。对于x86兼容硬件,您只需要为每个4096字节段写入一个字节,因为页面大小为4096,而硬件无法为较小的段实现写时复制行为;一旦修改了一个字节,整个4096字节段称为页面必须为您的进程保留。我不知道是否有任何现代CPU支持小于4096字节的页面。现代Intel CPU除了4096字节的页面外,还支持2 MB和1 GB页面,但很少使用1 GB页面,因为使用2 MB页面的开销对于任何合理的RAM数量都足够小。如果您的系统有数百TB的RAM,则1 GB页面可能是有意义的。
因此,基本上您的程序只测试了保留虚拟内存而从未使用该虚拟内存。您的操作系统可能针对此进行了特殊优化,避免需要超过4 KB的RAM来支持此操作。
除非你的目标是尝试测量由你的malloc()实现引起的开销,否则应避免尝试分配小于16-32字节的内存块。对于mmap()分配,在x86-64硬件上,最小可能的开销是每个分配8个字节,因为需要返回数据以将内存返回给操作系统,因此对于单个4字节分配来说,malloc()使用mmap()系统调用真的没有意义。
需要保留开销以跟踪内存分配,因为使用void free(void*)释放内存,因此内存分配例程必须在某个地方跟踪分配的内存段大小。许多malloc()实现还需要额外的元数据,如果它们需要跟踪任何内存地址,则每个地址需要8个字节。
如果您真的想要搜索系统的限制,您应该对malloc()失败的限制进行二进制搜索。实际上,您尝试分配...,1KB,2KB,4KB,8KB,…,32 GB,然后失败,您知道实际限制在16 GB和32 GB之间。然后可以将这个大小分成两半,并通过其他测试找出确切的限制。如果您进行这种搜索,最好总是释放任何成功的分配,并使用单个malloc()调用保留测试块。这也应该避免意外计算太多的malloc()开销,因为您最多一次只需要一个分配。
更新:正如Peter Cordes在评论中指出的那样,您的malloc()实现可能会将关于您分配的记录数据写入保留的RAM中,这会导致真正的内存被使用,并且可能导致系统开始交换得如此严重,以至于无法在任何合理的时间范围内恢复而不关机。如果您运行Linux并启用了“Magic SysRq”键,则可以按Alt+SysRq+f来杀死占用所有RAM的进程,然后系统会再次正常运行。可以编写malloc()实现,它通常不会触及通过brk()分配的RAM,我假设您将使用其中之一。(这种实现方式会将内存分配到2^n大小的段中,并且所有类似大小的段都保留在同一地址范围内。稍后调用free()时,malloc()实现从地址中知道分配的大小,并将有关空闲内存段的记录保存在单个位置的单独位图中。)在Linux的情况下,触及保留页面以进行内部记录的malloc()实现称为使内存变脏,这会因为写时复制处理而防止共享内存页面。 为什么循环在任何时间点都没有中断? 如果您的操作系统实现了上述特殊行为,并且您正在运行64位系统,则您不会在任何合理的时间范围内耗尽虚拟内存,因此您的循环似乎是无限的。

为什么没有分配失败?

因为您实际上并没有使用内存,所以您只是在分配虚拟内存。您基本上是在增加您的进程允许的最大指针值,但由于您从未访问过该内存,操作系统从未打扰为您的进程保留任何物理内存。

如果您正在运行Linux,并希望系统强制执行虚拟内存使用以匹配实际可用内存,则必须将/proc/sys/vm/overcommit_memory的内核设置写入2,并可能还要调整overcommit_ratio。请参见https://unix.stackexchange.com/q/441364/20336了解有关Linux上内存超额提交的详细信息。据我所知,Windows也实现了超额提交,但我不知道如何调整其行为。


1
有趣的是,我一直在想是否可以使用地址来确定大小,而不需要每次分配都进行簿记。很酷听到有些人实际上确实这样做了,我看到这对于一些常见的大小特别是小的2的幂分配会非常有效。 - Peter Cordes
我从未进行过检查,但我的理解是GNU libc实现了类似我描述的malloc(),主要是为了避免每个分配的记账。 - Mikko Rantalainen
1
我只看过较大的分配,特别是超过mmap阈值的分配。然后它通常会返回一个指针,即p%4096 == 16,因此我认为它必须在要返回的指针之前放置书记信息(例如大小),该信息通过16对齐以满足x86-64 SysV中的alignof(max_align_t)== 16。不幸的是,这对于AVX(32字节向量)始终是错误对齐的。我曾经认为它对于小的分配也会以同样的方式工作,但也许不是。从OP描述的症状来看,他们的Windows malloc确实是这样工作的。 - Peter Cordes
1
我在x86-64 Arch Linux上使用了glibc 2.35进行测试; 它会触及所有使用brk分配的页面,并使用4字节mallocs。 我用一个最大达到1000万次迭代的循环替换了OP的无限循环,仍然使用4字节分配,然后在其后放置了sleep(100)以便我可以观察RSS。 使用-O1或更低级别编译,从而不会优化未使用的malloc,RSS为306.1 MiB,VIRT为307。 因此,每一页都被触摸,并且它每个4字节malloc分配大约32字节。(306.1 MiB / 10e6是32.09)。 可能有16个用于簿记+对齐,另外16个用于分配+对齐填充。 - Peter Cordes
使用malloc(16)进行的另一个测试基本上具有相同的常驻集大小,即305.9 MiB(由物理RAM支持的内存量,即如果没有交换出任何内容,则脏数据量)。这是预期的。更有趣的是,malloc(17)也产生相同的结果,每个malloc(17)malloc(24)使用32字节。malloc(25)增加到458.6 MiB,因此每个分配使用48字节。因此,记账开销为8字节,可能只是一个大小。再次强调,这全部都是在x86-64 Linux上的glibc 2.35。 - Peter Cordes
显示剩余4条评论

1
我在Linux上使用/proc/meminfo中的MemAvailable参数来初步估计可用的RAM,然后使用二分法进行确认。我的目的是确定可以使用malloc来申请的最大物理RAM,但这仍然有些技巧性,并且可能会导致交换,尽管实际上它似乎一直表现得相当良好,至少在我使用的64位树莓派4系统上如此。请查看https://github.com/gtoal/biggest-malloc,并告诉我它对您是否有效,如果无效,请说明是哪个Linux版本、哪种内存以及出现了什么问题?(请在GitHub上发布而不是在这里。谢谢。)

-3

当您第一次分配任何大小到 *p 时,每次离开该内存以不被引用。这意味着

在一个时间段内,您的程序仅分配4字节的内存

那么您如何认为您已经使用了整个RAM,这就是 SWAP设备(HDD上的临时空间)没有讨论的原因。我知道一种内存管理算法,当没有一个程序引用内存块时,该块就有资格为程序内存请求分配使用。这就是为什么您只是让 RAM驱动器 忙碌,这也是它无法给其他程序服务的机会。这也是悬垂引用问题。

答案:最多可以分配与内存大小相同的内存。因为没有程序可以访问交换设备。

我希望您所有的问题都得到了令人满意的答案。


3
那是完全不正确的。我尝试在循环中运行8Gb的分配,它成功运行了16382次分配,相当于128Tb - 至少我没有那么多的内存。事实上,交换空间也没有那么多(请参见我对已接受答案的评论)。 - skyking
因为可能是压缩内存的原因? - JustinCB
你需要忘记那块内存,这里没有进行引用计数。即使没有任何指针指向它,该内存也不会被释放。 - olivecoder
@JustinCB:不会,因为过度承诺和惰性分配:实际上不会为那些虚拟页面分配物理页面,直到/除非它们被触及。只读访问将使页面写时复制映射到共享的零页。(我不知道这里的“未引用”是指“未触及”,还是描述了内存泄漏。)无论如何,这个答案是不正确的,未触及的分配仅受虚拟地址空间的限制(是的,在x86-64上,大约128 TiB是规范范围的低半部分,具有48位虚拟添加),只要你分配足够大的块,而不是物理RAM。 - Peter Cordes

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