堆内存分配

15
如果我在程序中使用malloc()动态分配内存,但在程序运行期间没有释放内存,那么动态分配的内存将会在程序终止后被释放吗?
如果没有释放,然后我一遍又一遍地执行同一个程序,它会每次都分配不同的内存块吗?如果是这样的话,我该如何释放那些内存?
注:我能想到的一个答案是重新启动正在执行该程序的计算机。但如果我在远程计算机上执行该程序,重新启动不是一个选项怎么办?

2
你确定重新启动会释放内存吗?;) ;) - stakx - no longer contributing
1
@stakx 我会断电至少1小时。 - huysentruitw
3
程序终止后动态分配的内存会怎样? - huysentruitw
@stakx 是的,我猜是这样的 :| - user4418808
我的疑问是,如果操作系统在进程终止时释放动态内存,就像释放静态分配的内存一样。那么在这种情况下,动态内存的释放不就类似于静态内存的释放吗? - user4418808
6个回答

19
简短回答:一旦您的进程终止,任何合理的操作系统都会释放该进程分配的所有内存。因此,当您多次重新启动进程时,不会累积内存分配。
进程和内存管理通常是操作系统的责任,因此分配的内存在进程终止后是否被释放实际上取决于操作系统。不同的操作系统可以以不同的方式处理内存管理。
话虽如此,任何合理的操作系统(尤其是多任务操作系统)都将在进程终止后释放该进程分配的所有内存。
我假设这背后的原因是操作系统必须能够优雅地处理不规则情况:
- 恶意程序(例如故意不释放其内存的程序,希望影响它运行的系统) - 异常程序终止(即程序意外结束的情况,因此可能无法显式释放其动态分配的内存)
任何值得信赖的操作系统都必须能够处理此类情况。它必须隔离系统的其他部分(例如自身和其他正在运行的进程)与错误的进程。如果没有,则进程的内存泄漏将传播到系统。这意味着操作系统会泄漏内存(通常被视为漏洞)。
保护系统免受内存泄漏的一种方法是确保一旦进程结束,它所使用的所有内存(以及可能的其他资源)都被释放。

谢谢你的答案,stakx。 - user4418808
@EOF:是的,我想规则总有例外。 (这个答案提到了另一个例外,即进程分叉。)但由于问题集中在“malloc”上,我认为只谈论一般情况是公平的。 - stakx - no longer contributing

8
无论内存是静态分配还是动态分配,程序在终止时都应该释放程序分配的所有内存。唯一的例外是,如果进程被分叉到另一个进程中。
如果您没有显式地使用free释放您所执行的任何malloc的内存,那么它将一直保持分配状态,直到进程终止。

静态分配的内存由编译器释放,但动态内存释放是程序员的责任。假如我编写了一个名为test.c的程序并编译生成了a.out文件,当运行a.out时,当控制权到达代码的最后一行时程序将被终止。那么,当程序终止时,使用malloc函数在test.c中分配的内存会自动释放吗? - user4418808
2
@RohitSaluja 编译器不会释放程序的任何内存。正如Mureinik的答案所说:当进程终止时,操作系统会释放其内存。 - BlackJack
@BlackJack 所以当进程自行终止或被强制终止时,操作系统也会释放动态分配的内存吗? - user4418808
1
@RohitSaluja 一句话 - 是的。 - Mureinik

3
即使您的操作系统在 exit() 后进行清理,退出系统调用通常会被 exit() 函数包装。下面是一些伪代码,从多个 libc 实现中学习而来,演示了围绕 main() 发生的可能导致问题的情况。
//unfortunately gcc has no builtin for stack pointer, so we use assembly
#ifdef __x86_64__
   #define STACK_POINTER "rsp"
#elif defined __i386__
   #define STACK_POINTER "esp"
#elif defined __aarch64__
   #define STACK_POINTER "x13"
#elif defined __arm__
   #define STACK_POINTER "r13"
#else
  #define STACK_POINTER "sp" //most commonly used name on other arches
#endif
char **environ;
void exit(int);
int main(int,char**,char**); 
_Noreturn void _start(void){ 
   register long *sp __asm__( STACK_POINTER ); 
   //if you don't use argc, argv or envp/environ, just remove them
   long argc = *sp;
   char **argv = (char **)(sp + 1);
   environ = (char **)(sp + argc + 1);
   //init routines for threads, dynamic linker, etc... go here
   exit(main((int)argc, argv, environ));
   __builtin_unreachable(); //or for(;;); to shut up compiler warnings
}

请注意,exit是使用main的返回值调用的。在没有动态链接器或线程的静态构建中,exit()可以直接内联syscall(__NR_exit,main(...));然而,如果您的libc使用了一个对exit()进行包装的*_fini()例程(大多数libc实现都这样做),在main()终止后仍然需要调用1个函数。
恶意程序可以LD_PRELOAD exit()或其调用的任何程序,并将其变成一种永远不会释放其内存的僵尸进程。
即使在调用exit()之前执行free(),该进程仍然会消耗一些内存(基本上是可执行文件的大小以及未被其他进程使用的共享库的一定程度),但某些操作系统可以重复使用非malloc()的内存来加载相同程序的后续版本,从而使您可以运行数月而不会注意到僵尸进程。
顺便说一句,除了dietlibc(构建为静态库时)和我只发布在Puppy Linux论坛上的部分静态库.h之外,大多数libc实现都具有某种形式的exit()包装器。

2
如果我在程序中使用malloc()动态分配内存,但在程序运行期间没有释放内存,那么动态分配的内存会在程序终止后被释放吗?
操作系统将释放通过malloc分配的内存以供其他系统使用。这比你的问题听起来要复杂得多,因为进程使用的物理内存可能会被写入磁盘(页面换出)。但是在Windows、Unix(Linux、MAC OS X、iOS、Android)中,系统将释放其已提交给进程的资源。
或者,如果它没有被释放,并且我一遍又一遍地执行同一个程序,它会每次分配不同的内存块吗?如果是这样,我该如何释放那块内存?
每次启动程序都会获得新的一组内存。这些内存来自系统,并作为虚拟地址提供。现代操作系统使用地址空间布局随机化(ASLR)作为安全功能,这意味着堆应该在每次启动程序时提供唯一的地址。但是由于其他运行的资源已经被整理好了,所以没有必要释放那块内存。
正如你所注意到的,如果下一次运行无法跟踪已经分配的资源的位置,那么它如何能够释放这些资源呢?此外,请注意,您可以同时运行多个程序,分配的内存可能会重叠 - 每个程序可能会看到相同的地址分配,但这是“虚拟内存”,操作系统独立地设置了每个进程,因此它似乎使用相同的内存,但与每个进程相关联的RAM是独立的。当程序执行时不释放内存,在Windows和Unix以及可能的任何其他合理的操作系统上都“可行”。
不释放内存的好处是,操作系统会保留分配给进程的大型内存块的列表,而malloc库则保留了分配给malloc的小内存块的表。
通过不释放内存,当进程终止时,您将节省处理这些小列表的工作。在某些情况下甚至建议这样做(例如MSDN:服务控制处理程序建议通过不释放内存来处理SERVICE_CONTROL_SHUTDOWN)。
但是,不释放内存的缺点也很明显。
像valgrind和应用程序验证器这样的程序通过监视分配给进程的内存并报告泄漏来检查程序的正确性。
当您不释放内存时,它们会报告很多噪音,使得难以找到无意中的泄漏。如果您在循环内泄漏内存,这将非常重要,因为这会限制程序可以处理的任务大小。
在我的职业生涯中,我曾几次将进程转换为共享对象/dll。这些是有问题的转换,因为预计由OS进程终止处理的泄漏开始在“main”生命周期之外生存。

1
作为操作系统的“大脑”,内核有许多职责。其中内存管理是内核的一个功能。内核可以完全访问系统的内存,并允许进程按需安全地访问该内存。通常,实现这一点的第一步是虚拟寻址,通常通过分页和/或分段实现。虚拟寻址使内核能够将给定的物理地址显示为另一个地址,即虚拟地址。不同的进程可能具有不同的虚拟地址空间;一个进程在特定(虚拟)地址访问的内存可能与另一个进程在相同地址访问的内存不同。这使得每个程序都可以表现得好像它是唯一运行的(除了内核),从而防止应用程序彼此崩溃。

内存分配

malloc

中分配一块内存块。

. .NET 等效函数:不适用。要调用标准的 C 函数,请使用PInvoke


堆内存

堆是计算机内存的一个区域,不会自动管理,并且CPU对其管理不太紧密。它是一个更自由浮动的内存区域(并且更大)。要在堆上分配内存,必须使用内置的C函数malloc()或calloc()。一旦你在堆上分配了内存,你就需要使用free()来释放那些不再需要的内存。如果你没有这样做,你的程序将出现所谓的内存泄漏。也就是说,堆上的内存仍然被保留下来(并且其他进程无法使用)。
内存泄漏 对于Windows 当一个进程从分页或非分页池中分配内存,但不释放内存时,就会发生内存泄漏。因此,这些有限的内存池随着时间的推移而枯竭,导致Windows变慢。如果内存完全耗尽,可能会导致失败。

防止Windows应用程序中的内存泄漏

记忆泄漏是一类错误,当应用程序不再需要内存时,它无法释放内存。随着时间的推移,记忆泄漏会影响特定应用程序以及操作系统的性能。如果存在大量泄漏,则可能由于过度分页而导致响应时间不可接受。最终,应用程序以及操作系统的其他部分都将经历故障。

Windows将在进程终止时释放应用程序分配的所有内存,因此短期运行的应用程序不会对整个系统性能产生显著影响。然而,长时间运行的进程(如服务或甚至资源管理器插件)中的泄漏可能会极大地影响系统可靠性,并可能迫使用户重新启动Windows,以使系统再次可用。

应用程序可以通过多种方式代表它们分配内存。如果未在使用后释放,则每种分配类型都可能导致泄漏。
以下是常见分配模式的一些示例:
  • 通过HeapAlloc函数或其C/C++运行时等价物mallocnew分配堆内存

  • 通过VirtualAlloc函数直接从操作系统分配内存。

  • 由Kernel32 APIs创建的内核句柄,例如CreateFileCreateEventCreateThread,代表应用程序持有内核内存

  • 由User32和Gdi32 APIs创建的GDI和USER句柄(默认情况下,每个进程有10,000个句柄的配额)

对于Linux

memprof是一个用于分析内存使用情况并查找内存泄漏的工具。它可以生成一个分析报告,显示每个函数分配了多少内存。此外,它还可以扫描内存并查找已经分配但不再被任何地方引用的块。


0

由malloc分配的内存需要由分配程序释放。如果不释放并继续分配内存,那么程序将在某一时刻耗尽允许的内存分配并抛出分段或内存不足错误。每个由malloc分配的内存集都需要伴随着free。


1
虽然你说的确实是正确的,但它并没有回答当前的问题,该问题询问一个进程终止之后会发生什么。而你的回答则侧重于进程结束之前应该发生什么。 - stakx - no longer contributing
当C语言进程在未释放内存的情况下终止时,可用于其他进程的内存将减少,并且会出现无法分配更多内存的情况,即malloc将失败,即堆内存已满。 - abhishekl
这与C编程语言本身无关。您所描述的行为可能是由于异常糟糕的C运行时库故意错误地执行操作...或者是由于可怕的操作系统。但我敢说,您的说法在一般情况下是事实上不正确的。据我所知,现今的操作系统确实会在进程终止后清理资源。 - stakx - no longer contributing

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