什么是内存碎片?

时间:2010-09-22 14:49:09

标签: c++ memory heap fragmentation

我听说在C ++动态内存分配的上下文中使用了几次“内存碎片”这个术语。我发现了一些关于如何处理内存碎片的问题,但找不到直接处理它本身的问题。所以:

  • 什么是内存碎片?
  • 如何判断内存碎片是否对我的应用程序造成问题?什么样的计划最容易受到影响?
  • 处理内存碎片的常用方法有哪些?

此外:

  • 我听说过使用动态分配可能会增加内存碎片。这是真的?在C ++的上下文中,我理解所有标准容器(std :: string,std :: vector等)都使用动态内存分配。如果在整个程序中使用它们(特别是std :: string),内存碎片是否更有可能成为问题?
  • 如何在STL繁重的应用程序中处理内存碎片?

12 个答案:

答案 0 :(得分:278)

想象一下,你有一个“大”(32字节)可用内存扩展区:

----------------------------------
|                                |
----------------------------------

现在,分配一些(5个分配):

----------------------------------
|aaaabbccccccddeeee              |
----------------------------------

现在,释放前四个分配但不是第五个分配:

----------------------------------
|              eeee              |
----------------------------------

现在,尝试分配16个字节。哎呀,我不能,尽管那几乎是免费的两倍。

在具有虚拟内存的系统上,碎片问题不像您想象的那么严重,因为大型分配只需要在虚拟地址空间中连续,而不是在物理中地址空间。所以在我的例子中,如果我有一个页面大小为2字节的虚拟内存,那么我可以毫无问题地进行16字节分配。物理内存看起来像这样:

----------------------------------
|ffffffffffffffeeeeff            |
----------------------------------

而虚拟内存(更大)可能如下所示:

------------------------------------------------------...
|              eeeeffffffffffffffff                   
------------------------------------------------------...

内存碎片的典型症状是你尝试分配一个大块但你不能,即使你看起来有足够的内存空闲。另一个可能的后果是进程无法将内存释放回OS(因为在OS中分配的所有块中仍然存在一些对象,即使这些块现在大多未使用)。

通过根据不同区域的大小和/或预期生命周期分配对象来防止C ++中的内存碎片的策略。因此,如果您要创建大量对象并在以后将它们全部销毁,请从内存池中分配它们。你在它们之间进行的任何其他分配都不会来自池,因此它们不会位于它们之间的内存中,因此内存不会因此而被分段。

通常你不需要太担心,除非你的程序长时间运行并进行大量的分配和释放。当你拥有短寿命和长寿命物体的混合物时,你的风险最大,但即便如此,malloc也会尽力提供帮助。基本上,忽略它,直到你的程序有分配失败或意外导致系统内存不足(在测试中捕获它,优先!)。

标准库并不比分配内存的任何其他内容差,标准容器都有Alloc模板参数,如果绝对必要,您可以使用它来微调其分配策略。

答案 1 :(得分:70)

  

什么是内存碎片?

内存碎片是指您的大部分内存分配在大量非连续的块或块中 - 使您的总内存中有很大一部分未分配,但在大多数典型情况下无法使用。这会导致内存不足或分配错误(即malloc返回null)。

最简单的思考方法是想象你有一个很大的空墙需要放置不同大小的 。每张照片占用一定的尺寸,你显然不能将它分成小块以使其适合。你需要在墙上留一个空白点,图片的大小,否则你就无法忍受。现在,如果你开始在墙上挂图片并且你不小心你如何安排它们,你很快会得到一个部分覆盖着图片的墙,即使你可能有空白点,大多数新图片也不适合因为它们比可用的点大。你仍然可以挂小图片,但大多数图片都不合适。所以你必须重新安排(紧凑)已经在墙上的那些,以腾出更多空间..

现在,想象一下墙是你的(堆)内存,图片是对象。那是内存碎片..

如何判断内存碎片对我的应用程序是否有问题?什么样的计划最容易受到影响?

你可能正在处理内存碎片的一个迹象是,如果你得到很多分配错误,特别是当使用内存的百分比很高时 - 但是你还没有用尽所有内存 - 所以从技术上来说你应该有你想要分配的物品有足够的空间。

当内存严重碎片化时,内存分配可能需要更长时间,因为内存分配器必须做更多工作才能为新对象找到合适的空间。如果反过来你有很多内存分配(你最后可能会因为内存碎片而这么做),分配时间甚至可能导致明显的延迟。

处理内存碎片的常用方法有哪些?

使用好的算法来分配内存。而不是为许多小对象分配内存,为这些较小对象的连续数组预先分配内存。分配内存时有时会有点浪费,可以提高性能,并且可以省去处理内存碎片的麻烦。

答案 2 :(得分:23)

内存碎片与磁盘碎片的概念相同:它指的是浪费的空间,因为使用的区域没有足够紧密地组合在一起。

假设一个简单的玩具示例,你有10个字节的内存:

 |   |   |   |   |   |   |   |   |   |   |
   0   1   2   3   4   5   6   7   8   9

现在让我们分配三个三字节块,名称A,B和C:

 | A | A | A | B | B | B | C | C | C |   |
   0   1   2   3   4   5   6   7   8   9

现在解除分配块B:

 | A | A | A |   |   |   | C | C | C |   |
   0   1   2   3   4   5   6   7   8   9

现在如果我们尝试分配一个四字节的块D会发生什么?好吧,我们有四个字节的内存空闲,但我们没有四个连续的字节的内存空闲,所以我们不能分配D!这是对内存的低效使用,因为我们应该能够存储D,但我们无法做到。我们不能移动C来腾出空间,因为我们程序中的某些变量很可能指向C,我们无法自动查找和更改所有这些值。

你怎么知道这是一个问题?好吧,最大的迹象是你的程序的虚拟内存大小远远大于你实际使用的内存量。在一个实际的例子中,你将有超过十个字节的内存,所以D只会从字节9开始分配,而字节3-5将保持未使用状态,除非你后来分配了三个字节长或更小的东西。

在这个例子中,3个字节不是浪费,但考虑一个更加病态的情况,其中两个字节的两个分配,例如,在内存中相隔十兆字节,你需要分配一个块大小10兆字节+ 1字节。你必须要求操作系统获得超过10兆字节的虚拟内存才能做到这一点,即使你只有一个字节还没有足够的空间。

你是如何预防的?当您经常创建和销毁小物体时,最坏的情况往往会产生,因为这会产生“瑞士奶酪”效应,许多小物体被许多小孔分开,从而无法在这些洞中分配更大的物体。当你知道你将会这样做时,一个有效的策略是预先分配一大块内存作为小对象的池,然后手动管理该块中的小对象的创建,而不是让它默认的分配器处理它。

通常,您执行的分配越少,内存碎片的可能性就越小。但是,STL相当有效地处理了这个问题。如果你有一个字符串使用其当前分配的全部并且你附加一个字符,它不会简单地重新分配到它的当前长度加一,它加倍它的长度。这是“频繁小额分配池”策略的变体。该字符串占用了大量内存,因此它可以有效地处理重复的小幅增加,而无需重复进行小的重新分配。实际上所有的STL容器都是这样做的,所以通常你不必过于担心自动重新分配STL容器造成的碎片。

虽然STL容器当然不会在之间汇集内存,所以如果你要创建许多小容器(而不是经常调整大小的几个容器),你可能需要关注自己防止碎片的方式与任何经常创建的小对象STL相同。

答案 3 :(得分:14)

  
      
  • 什么是内存碎片?
  •   

内存碎片是内存变得无法使用的问题,即使它在理论上可用。碎片有两种类型:内部碎片是分配但不能使用的内存(例如,当内存以8字节块分配时,但程序只需要4个字节就会重复执行单个转义)。 外部碎片是空闲内存被分成许多小块的问题,因此尽管有足够的整体可用内存,但仍无法满足大的分配请求。

  
      
  • 如何判断内存碎片是否对我的应用程序造成问题?什么样的计划最容易受到影响?
  •   
如果您的程序使用的系统内存比实际的paylod数据要多得多(而且排除了内存泄漏),那么内存碎片是一个问题。

  
      
  • 处理内存碎片的常用方法有哪些?
  •   

使用一个好的内存分配器。 IIRC,那些使用“最适合”策略的人通常在避免碎片方面要优越得多,如果慢一点的话。但是,已经表明,对于任何分配策略,都存在病态最坏的情况。幸运的是,大多数应用程序的典型分配模式实际上对于分配器来说是相对良性的。如果您对细节感兴趣,那里有很多论文:

  • Paul R. Wilson,Mark S. Johnstone,Michael Neely和David Boles。动态存储分配:调查和严格审查。在1995年的会议录中 国际记忆管理研讨会,Springer Verlag LNCS,1995年
  • Mark S.Johnstone,Paul R. Wilson。内存碎片问题:解决了吗? 在ACM SIG-PLAN Notices,第34卷第3期,第26-36页,1999年
  • M.R。 Garey,R.L。Graham和J.D. Ullman。内存分配算法的最坏情况分析。 1972年第四届计算机理论ACM研讨会

答案 4 :(得分:9)

更新:
Google TCMalloc: Thread-Caching Malloc
已经发现在长时间运行的过程中非常善于处理碎片


我一直在开发一个服务器应用程序,该应用程序在HP-UX 11.23 / 11.31 ia64上存在内存碎片问题。

看起来像这样。有一个进程进行了内存分配和解除分配并运行了几天。即使没有内存泄漏,内存消耗也会不断增加。

关于我的经历。在HP-UX上,使用HP-UX gdb很容易找到内存碎片。您设置了一个断点,当您点击它时,您运行此命令:info heap并查看进程的所有内存分配和堆的总大小。然后你继续你的程序,然后一段时间后你再次达到了断点。你又做了info heap。如果堆的总大小较大但单独分配的数量和大小相同,则可能存在内存分配问题。如有必要,请在前几次检查。

我改善这种情况的方法是这样的。在我使用HP-UX gdb进行一些分析后,我发现内存问题是由于我使用std::vector来存储数据库中的某些类型的信息。 std::vector要求其数据必须保存在一个块中。我有一些基于std::vector的容器。这些容器定期重建。通常情况下,将新记录添加到数据库中,然后重新创建容器。而且由于重新创建的容器更大,它们不适合可用的可用内存块,运行时需要从操作系统中获取更大的块。因此,即使没有内存泄漏,该进程的内存消耗也会增加。当我更换容器时,我改善了这种情况。而不是std::vector我开始使用std::deque,它有不同的方式为数据分配内存。

我知道在HP-UX上避免内存碎片的方法之一是使用Small Block Allocator或使用MallocNextGen。在RedHat Linux上,默认的分配器似乎可以很好地分配很多小块。在Windows上有Low-fragmentation Heap,它解决了大量小分配的问题。

我的理解是,在STL繁重的应用程序中,您首先要发现问题。内存分配器(比如在libc中)实际上处理了许多小分配的问题,这对于std::string来说是典型的(例如在我的服务器应用程序中有很多STL字符串,但正如我在运行{{1}时看到的那样他们没有造成任何问题)。我的印象是你需要避免频繁的大量分配。不幸的是,有些情况下您无法避免它们并且必须更改代码。正如我所说,在切换到info heap时我改善了情况。如果你确定了你的记忆碎片,就可以更精确地谈论它。

答案 5 :(得分:6)

当您分配和解除分配许多不同大小的对象时,最有可能发生内存碎片。假设你在内存中有以下布局:

obj1 (10kb) | obj2(20kb) | obj3(5kb) | unused space (100kb)

现在,当释放obj2时,你有120kb的未使用内存,但你不能分配120kb的完整块,因为内存是碎片化的。

避免这种影响的常用技巧包括ring buffersobject pools。在STL的上下文中,像std::vector::reserve()这样的方法可以提供帮助。

答案 6 :(得分:6)

关于内存碎片的非常详细的答案可以在这里找到。

http://library.softwareverify.com/memory-fragmentation-your-worst-nightmare/

这是11年内存碎片问题的高潮,我一直在向人们提供有关内存碎片的问题,请访问softwareverify.com

答案 7 :(得分:3)

  

什么是内存碎片?

当您的应用使用动态内存时,它会分配并释放内存块。在开始时,应用程序的整个内存空间是一个连续的可用内存块。但是,当您分配和释放不同大小的块时,内存开始变为碎片,即不是一个大的连续空闲块和一些连续分配的块,而是会有一个已分配的空闲块混合。由于空闲块的大小有限,因此很难重用它们。例如。你可能有1000个字节的可用内存,但不能为100字节块分配内存,因为所有空闲块最多只能有50个字节。

另一个不可避免但但问题较少的碎片来源是,在大多数体系结构中,内存地址必须对齐到2,4,8等字节边界(即地址必须是2的倍数) ,4,8等)这意味着即使你有例如一个包含3个char字段的结构,由于每个字段与4字节边界对齐,您的结构可能大小为12而不是3。

  

如何判断内存碎片对我的应用程序是否有问题?什么样的计划最容易受到影响?

显而易见的答案是你得到了一个内存不足的例外。

显然,没有好的可移植方法来检测C ++应用程序中的内存碎片。有关详细信息,请参阅this answer

  

处理内存碎片的常用方法有哪些?

在C ++中很难,因为你在指针中使用直接内存地址,并且你无法控制谁引用特定的内存地址。因此,重新排列已分配的内存块(Java垃圾收集器的方式)不是一种选择。

自定义分配器可以通过管理较大块内存中的小对象分配,并重用该块中的空闲槽来提供帮助。

答案 8 :(得分:3)

这是傻瓜的超简化版本。

当对象在内存中创建时,它们会被添加到内存中已使用部分的末尾。

如果删除了不在内存使用部分末尾的对象,意味着此对象位于其他两个对象之间,则会创建一个“洞”。

这就是所谓的碎片化。

答案 9 :(得分:2)

当你想在堆上添加一个项目时,发生的事情是计算机必须搜索空间以适合该项目。这就是为什么在内存池或池化分配器上进行动态分配时可能会“减慢”速度。对于繁重的STL应用程序,如果您正在进行多线程处理,则可以使用Hoard allocatorTBB Intel版本。

现在,当内存碎片化时,可能会发生两件事:

  1. 必须有更多的搜索才能找到一个好的空间来粘贴“大”物体。也就是说,许多小物体分散在一起找到一个很好的重点内存可能在某些条件下很难(这些是极端的。)
  2. 内存不是一些容易阅读的实体。处理器仅限于它们能容纳多少以及在哪里。他们通过交换页面来实现这一点,如果他们需要的项目是一个地方,但当前地址是另一个。如果您经常不得不交换页面,处理速度会变慢(再次,这会影响性能的极端情况。)请参阅virtual memory上的帖子。

答案 10 :(得分:1)

发生内存碎片是因为请求了不同大小的内存块。考虑一个100字节的缓冲区。你请求两个字符,然后是一个整数。现在你释放两个字符,然后请求一个新的整数 - 但是这个整数不能适合两个字符的空间。该内存无法重复使用,因为它不在足够大的连续块中进行重新分配。最重要的是,你为你的字符调用了很多分配器开销。

基本上,在大多数系统中,内存只有一定大小的块。一旦拆分这些块,就不能重新加入它们,直到整个块被释放。当实际上只有一小部分块正在使用时,这可能导致整个块被使用。

减少堆碎片的主要方法是进行更大,更少的分配。在极端情况下,您可以使用能够移动对象的托管堆,至少在您自己的代码中。无论如何,从内存的角度来看,这完全消除了这个问题。显然移动物体等具有成本。实际上,如果您经常从堆中分配非常少量的数据,那么您确实遇到了问题。使用连续的容器(矢量,字符串等)并尽可能地在堆栈上分配(总是一个好主意的性能)是减少它的最佳方法。这也增加了缓存一致性,使您的应用程序运行得更快。

你应该记住的是,在32位x86桌面系统上,你有一个完整的2GB内存,它被分成4KB“页面”(非常确定所有x86系统的页面大小都是一样的)。您将不得不调用一些omgwtfbbq碎片来解决问题。碎片化确实是过去的一个问题,因为对于绝大多数应用来说,现代堆已经过大了,并且存在能够承受它的系统,例如托管堆。

答案 11 :(得分:0)

最有可能遭受哪种程序的侵害?

与内存碎片相关的问题的一个很好的例子(是令人恐惧的)是开发和发行了Stardock的计算机游戏《 <元素>魔法战争》 。

>

该游戏是为32位/ 2GB内存构建的,必须对内存管理进行大量优化,才能使游戏在这2GB内存内运行。由于“优化”导致不断分配和取消分配,随着时间的推移,发生了堆内存碎片,并使游戏崩溃

YouTube上有一个"war story" interview