什么是内存碎片?


203

我听说过在C ++动态内存分配中使用“内存碎片”一词的次数。我发现了一些有关如何处理内存碎片的问题,但是找不到直接解决它本身的问题。所以:

  • 什么是内存碎片?
  • 如何判断内存碎片是否对我的应用程序造成问题?哪种程序最有可能受到影响?
  • 有什么好的常见方法可以处理内存碎片?

也:

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

1
非常好的答案,谢谢大家!
AshleysBrain

4
已经有了很多不错的答案,但是这是来自实际应用程序(Firefox)的一些图片,其中内存碎片是一个大问题:blog.pavlov.net/2007/11/10/memory-fragmentation
Marius

2
@MariusGedminas链接不再起作用,这就是为什么随同该链接提供简短摘要或使用该链接回答具有摘要的问题很重要
katta

当然可以,但是已经过去了五年了
rsethc

3
以下是Marius发布的链接的更新位置:pavlovdotnet.wordpress.com/2007/11/10/memory-fragmentation
TheGameiswar

Answers:


312

想象一下,您拥有“大”(32字节)的空闲内存空间:

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

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

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

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

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

现在,尝试分配16个字节。糟糕,即使有那么多的免费,我也无法做到。

在具有虚拟内存的系统上,碎片化的问题比您想象的要少,因为大型分配只需要在虚拟地址空间中是连续的,而不是在物理地址空间中是连续的。因此,在我的示例中,如果我的虚拟内存的页面大小为2字节,则可以毫无问题地进行16字节分配。物理内存如下所示:

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

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

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

内存碎片的经典症状是,即使您似乎有足够的可用内存,您还是尝试分配一个大块而您却无法分配。另一个可能的结果是该进程无法将内存释放回OS(因为它从OS分配的所有块中仍然有一些对象在使用中,即使这些块现在几乎未使用)。

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

通常,您不必担心太多,除非您的程序长时间运行并且需要进行大量分配和释放。在这种情况下,您面临的风险最大的是短寿命和长寿命的混合物,即使malloc那样,它也将尽最大努力为您提供帮助。基本上,请忽略它,直到您的程序出现分配失败或意外导致系统内存不足时为止(在测试中捕获此优先)!

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


1
那么每个字符都是一个字节?这会让您的“大笔钱” == 32字节(我想-不算在内):)很好的例子,但是在最后一行之前提到单位会有所帮助。:)
jalf

1
@杰夫:是的。我根本不提单位,然后在最后不得不意识到。在您发表评论时正在努力。
史蒂夫·杰索普

选择“答案”非常困难-这里有很多很棒的答案,我鼓励任何有兴趣的人阅读所有答案。不过,我认为您在这里已经涵盖了所有要点。
AshleysBrain 2010年

1
“标准库并不比分配内存的其他任何库差”。如果为true,那会很好,但是标准C ++模板(例如字符串和矢量)的实现在调整大小时可能会出现一些非常不理想的行为。例如,在旧版本的Visual Studio中,std :: string基本上通过重新分配1.5 * current_size(至最接近的8个字节)来调整大小。因此,如果您继续追加字符串,则可以非常轻松地消除堆,特别是在嵌入式系统上。最好的防御方法是保留您预期使用的空间量,以避免隐藏的重新分配。
洛克

1
@ du369:虚拟内存没有像物理内存那样严重碎片化。ffffffffffffffff是虚拟内存中的连续分配,但物理内存中不能存在这种连续分配。如果您希望查看它们是一样分散的,但是虚拟空间要大得多,那么请随意以这种方式查看它。重要的实际要点是,使用大量的虚拟地址空间通常足以忽略碎片,因此只要允许我进行16字节分配,它便会有所帮助。
史蒂夫·杰索普

73

什么是内存碎片?

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

对此进行考虑的最简单方法是,假设您有一堵空墙,需要放置各种尺寸的图片。每张图片都占据一定的大小,您显然无法将其分成较小的部分以使其适合。您需要在墙上有一个空白的地方,图片的大小,否则您将无法放置它。现在,如果您开始将照片挂在墙上,而又不怎么安排它们,您很快就会遇到一堵部分覆盖了照片的墙壁,即使您可能有空白的地方,大多数新照片也无法容纳因为它们比可用的斑点大。您仍然可以挂上很小的照片,但是大多数都不适合。因此,您必须重新布置(紧凑)在墙上的那些,以便腾出更多空间。

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

如何判断内存碎片是否对我的应用程序造成问题?哪种程序最有可能受到影响?

一个可能表明您正在处理内存碎片的迹象是,如果您遇到许多分配错误,尤其是在已用内存的百分比很高时-但不是您还没有用完所有内存-因此从技术上讲,您应该有足够的空间您尝试分配的对象。

当内存严重碎片化时,内存分配可能会花费更长的时间,因为内存分配器必须做更多的工作才能为新对象找到合适的空间。反过来,如果您有许多内存分配(自从内存碎片结束以来您可能会这样做),分配时间甚至可能导致明显的延迟。

有什么好的常见方法可以处理内存碎片?

使用良好的算法分配内存。与其为许多小对象分配内存,不如为那些小对象的连续数组分配内存。有时在分配内存时有点浪费,可以提高性能,并可以节省处理内存碎片的麻烦。


10
+1。我刚刚删除了我提出的答案,因为您的“墙上的图片”隐喻确实非常好,很清晰。
ctacke 2010年

如果您强调图片必须具有不同的尺寸这一事实,我会更喜欢。否则,不会发生碎片。
比约恩·波莱克斯(BjörnPollex)2010年

1
有趣的是,最近这些天的主内存数据库变得越来越实用(可用内存确实很大)。在这种情况下,值得注意的是,对于HDD而言,从RAM中读取连续行比将数据分段时要快得多。
比约恩·波莱克斯(BjörnPollex)2010年

1
与墙上的图片很像,但主记忆不是二维的!不过,很好的答案,谢谢。
AshleysBrain 2010年

24

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

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

 |   |   |   |   |   |   |   |   |   |   |
   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,但是我们无法存储D。而且我们无法移动C来腾出空间,因为程序中的某些变量很可能指向C,因此我们无法自动查找和更改所有这些值。

您怎么知道这是一个问题?好吧,最大的迹象是程序的虚拟内存大小比实际使用的内存量大得多。在一个实际的示例中,您将拥有超过十个字节的内存,因此D将从字节9开始分配,而字节3-5将保持未使用状态,除非您以后分配了三个字节或更小的字节。

在此示例中,3个字节不是浪费太多,而是考虑一种更病理的情况,例如,两个字节的两个分配在内存中相隔十兆字节,并且您需要分配一个大小为10兆字节的块+ 1个字节。您必须要求操作系统多提供10兆字节以上的虚拟内存才能做到这一点,即使您已经拥有足够的空间也只差一个字节。

您如何预防呢?当您频繁创建和销毁小对象时,最坏的情况往往会发生,因为这往往会产生“瑞士奶酪”效果,其中许多小对象之间有许多小孔,因此无法在这些孔中分配更大的对象。当您知道将要执行此操作时,一种有效的策略是为小对象预先分配一个大内存块作为池,然后手动管理该块中小对象的创建,而不是让它默认分配器处理它。

通常,分配的次数越少,内存碎片的可能性就越小。但是,STL相当有效地处理了这一问题。如果您的字符串使用的是当前分配的全部内容,并且在其中附加了一个字符,则它不会简单地将其分配为当前长度加一个,而是将其长度加倍。这是“频繁分配少量资金的池”策略的一种变体。该字符串占用了大量内存,因此它可以有效地处理大小的重复小幅增加,而无需重复进行小的重新分配。实际上,所有STL容器都会执行此类操作,因此通常您不必担心由自动重新分配STL容器引起的碎片。

尽管STL容器当然不会彼此之间共享内存,所以如果您要创建许多小容器(而不是几个经常调整大小的容器),则可能需要以防止碎片的方式来担心自己适用于任何经常创建的小对象,无论是否为STL。


14
  • 什么是内存碎片?

内存碎片是即使理论上可用内存也无法使用的问题。碎片有两种:内部碎片是已分配但无法使用的内存(例如,当内存以8字节块分配时,但程序仅需要4字节时重复执行一次分配)。外部碎片是将空闲内存分成许多小块的问题,因此尽管有足够的总体空闲内存,但无法满足较大的分配请求。

  • 如何判断内存碎片是否对我的应用程序造成问题?哪种程序最有可能受到影响?

如果您的程序使用的系统内存比实际的Paylod数据所需的要多(并且您已排除内存泄漏),则内存碎片是一个问题。

  • 有什么好的常见方法可以处理内存碎片?

使用良好的内存分配器。在IIRC中,那些使用“最适合”策略的策略在避免碎片化方面通常要优越得多,即使速度稍慢一些。但是,也已经表明,对于任何分配策略,都存在病理性最坏情况。幸运的是,大多数应用程序的典型分配模式实际上对于分配器来说是相对良性的。如果您对详细信息感兴趣,可以找到很多论文:

  • 保罗·威尔逊(Paul R. Wilson),马克·约翰逊(Mark S.Johnstone),迈克尔·尼利(Michael Neely)和大卫·博尔斯(David Boles)。动态存储分配:调查和重要审查。在1995年国际内存管理研讨会的记录中,Springer Verlag LNCS,1995年
  • 马克·S·约翰斯通,保罗·R·威尔逊。内存碎片问题:解决了吗?在ACM SIG-PLAN通告中,第34卷第3期,第26-36页,1999年
  • 加雷先生,格雷厄姆(RL Graham)和乌尔曼(JD Ullman)。内存分配算法的最坏情况分析。在1972年第四届ACM计算理论研讨会上

9

更新:
Google TCMalloc:线程缓存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字符串,但是从运行中info heap可以看出,它们不会引起任何问题)。我的印象是您需要避免频繁的大量分配。不幸的是,在某些情况下,您无法避免它们而不得不更改代码。如我所说,我改用时的情况有所改善std::deque。如果您确定内存碎片,可以更精确地讨论它。


6

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

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

现在,当obj2释放时,您有120kb的未使用内存,但是由于内存是零散的,因此无法分配120kb的完整块。

避免这种影响的常用技术包括环形缓冲区对象池。在STL的上下文中,类似的方法std::vector::reserve()可以提供帮助。



3

什么是内存碎片?

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

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

如何判断内存碎片是否对我的应用程序造成问题?哪种程序最有可能受到影响?

显而易见的答案是您遇到了内存不足的异常。

显然,没有很好的可移植方法来检测C ++应用程序中的内存碎片。有关更多详细信息,请参见此答案

有什么好的常见方法可以处理内存碎片?

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

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


3

这是针对假人的超级简化版本。

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

如果删除了不在内存已用部分末尾的对象,这意味着该对象位于其他两个对象之间,它将创建一个“空洞”。

这就是所谓的碎片化。


2

当您要在堆上添加项目时,发生的情况是计算机必须搜索适合该项目的空间。这就是为什么在内存池上或没有使用池分配器进行动态分配时会降低速度的原因。对于繁重的STL应用程序,如果您正在执行多线程,则可以使用Hoard分配器TBB Intel版本。

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

  1. 必须进行更多搜索才能找到粘贴“大”物体的良好空间。也就是说,由于散布着许多小对象,很难在某些条件下找到一块连续的漂亮内存(这些极端)。
  2. 内存不是一些容易读取的实体。处理器的限制是它们可以容纳多少以及放置在何处。如果他们需要的项目在一个地方而当前地址在另一个地方,他们通过交换页面来实现。如果您经常需要交换页面,则处理速度可能会变慢(同样,在极端情况下,这会影响性能。)请参阅虚拟内存上的此发布。

1

发生内存碎片是因为请求了不同大小的内存块。考虑一个100字节的缓冲区。您要求两个字符,然后是一个整数。现在,您释放了两个字符,然后请求一个新的整数-但该整数不能容纳在两个字符的空间中。该内存不能重用,因为它没有足够大的连续块来重新分配。最重要的是,您为字符调用了许多分配器开销。

本质上,在大多数系统上,内存仅以一定大小的块进来。一旦拆分了这些块,就无法重新加入它们,直到释放整个块。当实际上只有一小部分模块在使用时,这可能会导致整个模块被使用。

减少堆碎片的主要方法是进行更大,更不频繁的分配。在极端情况下,您可以使用托管堆,该托管堆至少可以在自己的代码内移动对象。无论如何,从内存的角度来看,这完全解决了问题。显然,移动物体等具有成本。实际上,只有经常从堆中分配很少的量时,您才真正有问题。使用连续容器(向量,字符串等)并尽可能多地在栈上分配资源(对于性能而言始终是个好主意)是减少它的最佳方法。这也提高了缓存的一致性,从而使您的应用程序运行得更快。

您应该记住的是,在32位x86桌面系统上,您拥有整个2GB的内存,该内存被拆分为4KB的“页面”(确保页面大小在所有x86系统上都相同)。您将必须调用一些omgwtfbbq碎片才能出现问题。碎片确实是过去的问题,因为对于大多数应用程序来说,现代堆的容量过大,并且有很多能够承受碎片的系统,例如托管堆。


0

哪种程序最有可能受到影响?

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

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

YouTube上有一个“战争故事”采访

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.