什么是内存碎片?

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

  • 什么是内存碎片?
  • 如何判断内存碎片是否是我的应用程序的一个问题?什么样的程序最可能受到影响?
  • 处理内存碎片的常用方法是什么?

另外:

  • 我听说经常使用动态分配会增加内存碎片。这是真的吗?在c++环境中,我知道所有的标准容器(std::string, std::vector等)都使用动态内存分配。如果在整个程序中使用这些(特别是std::string),内存碎片更可能是一个问题吗?
  • 在stl较多的应用程序中如何处理内存碎片?
113114 次浏览

什么是内存碎片?

内存碎片是指当您的大部分内存被分配为大量不连续的块或块时——留下很大比例的内存未分配,但在大多数典型场景下无法使用。这将导致内存溢出异常或分配错误(即malloc返回null)。

思考这个问题最简单的方法是想象你有一个大的空墙,你需要把图片大小不一放在上面。每张图片都有一定的大小,你显然不能把它拆成更小的碎片。你需要在墙上有一个空的位置,画的大小,否则你不能把它挂起来。现在,如果你开始在墙上挂照片,你不小心如何安排他们,你很快就会有一个部分被照片覆盖的墙壁,即使你可能有空的位置,大多数新照片都放不下,因为它们比可用的位置大。你仍然可以挂很小的画,但大多数都放不下。所以你得把已经挂在墙上的重新整理(压缩)好腾出地方来放更多的。

现在,想象墙是你的(堆)内存,图片是物体。这就是内存碎片。

如何判断内存碎片是否是我的应用程序的一个问题?什么样的程序最可能受到影响?

您可能正在处理内存碎片的一个明显迹象是,如果您得到许多分配错误,特别是当已使用内存的百分比很高时(但不是您还没有使用完所有内存),那么从技术上讲,您应该有足够的空间用于您试图分配的对象。

当内存严重碎片化时,内存分配可能需要更长的时间,因为内存分配器必须做更多的工作来为新对象找到合适的空间。如果您有许多内存分配(您可能会这样做,因为您最终会产生内存碎片),分配时间甚至可能会导致明显的延迟。

处理内存碎片的常用方法是什么?

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

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

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

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

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

什么是内存碎片?

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

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

如何判断内存碎片是否是我的应用程序的一个问题?什么样的程序最可能受到影响?

最明显的答案是内存不足异常。

显然,在c++应用程序中,没有一种好的便携式方法来检测内存碎片。更多细节参见这个答案

处理内存碎片的常用方法是什么?

这在c++中很困难,因为你在指针中使用直接内存地址,你无法控制谁引用特定的内存地址。因此,重新安排已分配的内存块(Java垃圾收集器的方式)是不可取的。

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

想象一下你有一个“大”;(32字节)空闲内存的扩展:

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

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

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

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

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

现在,尝试分配16个字节。哦,我不能,尽管有近两倍的免费。

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

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

而虚拟内存(要大得多)可能是这样的:

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

内存碎片的典型症状是,您试图分配一个大块,但您不能,即使您看起来有足够的空闲内存。另一个可能的后果是进程无法将内存释放回操作系统(因为它从操作系统中分配的每个大块,用于malloc等细分,其中都有一些剩余,即使每个块的大部分现在都未使用)。

在c++中,防止内存碎片的策略是根据对象的大小和/或它们的预期生命期从不同的区域分配对象。如果你要创建很多对象,然后一起销毁它们,从内存池中分配它们。在它们之间执行的任何其他分配都不会来自池,因此不会位于它们之间的内存中,因此内存不会因此而被碎片化。或者,如果你要分配很多相同大小的对象,那么就从同一个池中分配它们。那么池中的一段空闲空间永远不会小于您试图从池中分配的大小。

一般来说,您不需要太担心它,除非您的程序是长时间运行的,并且进行了大量的分配和释放。当你有短命和长寿对象的混合物时,你的风险最大,但即使这样malloc也会尽最大努力提供帮助。基本上,忽略它,直到您的程序出现分配失败或意外地导致系统内存不足(在测试中捕获它,这是首选!)。

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

当你想在堆上添加一项时,会发生的事情是计算机必须搜索空间来容纳该项。这就是为什么动态分配不在内存池上执行或使用池分配程序会“减慢”速度的原因。对于一个沉重的STL应用程序,如果你在做多线程,有囤积分配器TBB英特尔版本。

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

  1. 为了找到一个合适的空间来放置“大型”物体,将不得不进行更多的搜索。也就是说,由于许多小对象分散在各处,在某些条件下很难找到一个良好的相邻内存块(这些都是极端情况)。
  2. 内存不是容易读取的实体。处理器的容量和存储位置受到限制。如果他们需要的物品在一个地方,而当前地址在另一个地方,他们就会交换页面。如果必须经常交换页面,处理速度可能会变慢(同样,在极端情况下,这会影响性能)。请看虚拟内存上的帖子。
  • 什么是内存碎片?

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

  • 如何判断内存碎片是否是我的应用程序的一个问题?什么样的程序最可能受到影响?

如果您的程序使用的系统内存远远超过其实际有效负载数据所需的内存(并且您已经排除了内存泄漏),那么内存碎片就是一个问题。

  • 处理内存碎片的常用方法是什么?

使用一个好的内存分配器。IIRC,那些使用“最适合”的;策略通常在避免碎片化方面要优越得多,只是速度稍慢一些。然而,事实也表明,对于任何配置策略,都存在病态的最坏情况。幸运的是,对于分配器来说,大多数应用程序的典型分配模式实际上是相对友好的。如果你对细节感兴趣,这里有一堆文件:

    保罗·r·威尔逊,马克·s·约翰斯通,迈克尔·尼利和大卫·博尔斯。动态存储分配:调查和关键回顾。1995年会议记录 内存管理国际研讨会,施普林格Verlag LNCS, 1995 马克·s·约翰斯通,保罗·r·威尔逊。内存碎片问题:解决了吗? ACM SIG-PLAN公告,第34卷第3期,第26-36页,1999
  • m·r·加里,r·l·格雷厄姆和j·d·厄尔曼。内存分配算法的最差情况分析。在第四届ACM年度计算理论研讨会上,1972

内存碎片是因为请求不同大小的内存块。考虑一个100字节的缓冲区。您请求两个字符,然后是一个整数。现在释放这两个字符,然后请求一个新的整数——但是这个整数不能容纳这两个字符的空间。该内存不能被重用,因为它不在一个足够大的连续块中,无法重新分配。除此之外,还为字符调用了大量分配器开销。

从本质上讲,在大多数系统上,内存只以一定大小的块形式存在。一旦你把这些块分开,它们不能重新连接,直到整个块被释放。这可能导致整个区块都在使用,而实际上只有一小部分区块在使用。

减少堆碎片的主要方法是进行更大、更少的分配。在极端情况下,您可以使用能够移动对象的托管堆,至少在您自己的代码中是这样。这完全消除了问题——至少从内存的角度来看是这样。显然,移动物体是有代价的。实际上,只有在经常从堆中分配非常小的量时才会真正遇到问题。使用连续的容器(vector, string等)并在堆栈上尽可能多地分配(对于性能来说总是一个好主意)是减少它的最佳方法。这还增加了缓存一致性,从而使应用程序运行得更快。

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

内存碎片与磁盘碎片是同一个概念:它指的是由于正在使用的区域没有足够紧密地打包在一起而浪费的空间。

举个简单的例子,假设你有10个字节的内存:

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

现在让我们分配三个3字节的块,命名为A, B和C:

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

现在释放block B:

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

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

你怎么知道这是个问题?那么,最大的迹象就是程序的虚拟内存大小比实际使用的内存量大得多。在现实世界的示例中,您将拥有超过10个字节的内存,因此D将从字节9开始分配,而字节3-5将一直未使用,除非稍后分配长度为3字节或更小的内存。

在这个例子中,3个字节并不是很大的浪费,但是考虑一个更病态的情况,两个字节的分配,例如,内存中间隔10兆字节,而您需要分配一个大小为10兆字节+ 1字节的块。你必须要求操作系统提供超过10兆字节的虚拟内存,即使你只差一个字节就有足够的空间了。

如何预防呢?最糟糕的情况往往出现在频繁创建和销毁小对象时,因为这往往会产生一种“瑞士奶酪”效应,许多小对象被许多小孔隔开,使得不可能在这些小孔中分配更大的对象。当您知道要这样做时,一个有效的策略是预先分配一个大内存块作为小对象的池,然后手动管理该块内小对象的创建,而不是让默认分配器处理它。

一般来说,分配越少,内存碎片化的可能性就越小。然而,STL相当有效地处理了这个问题。如果你有一个字符串,它正在使用它当前分配的全部,你附加一个字符给它,它不会简单地重新分配到它当前的长度加1,它会双打它的长度。这是“频繁小额分配池”策略的一种变体。字符串占用了一大块内存,这样它就可以有效地处理重复的小的大小增加,而不需要重复的小的重新分配。事实上,所有的STL容器都做这类事情,所以通常你不需要太担心自动重新分配STL容器所引起的碎片。

当然,STL容器不会彼此共享之间的内存,所以如果你要创建许多小容器(而不是几个经常调整大小的容器),你可能必须像处理任何经常创建的小对象一样,注意防止碎片化,不管是不是STL。

更新:
TCMalloc:线程缓存Malloc谷歌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::deque而不是std::vector,它有一种不同的方式为数据分配内存。

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

我的理解是,在一个stl较多的应用程序中,你必须首先识别问题。内存分配器(如在libc中)实际上处理了大量小分配的问题,这是典型的std::string(例如,在我的服务器应用程序中有很多STL字符串,但正如我从运行info heap中看到的那样,它们不会引起任何问题)。我的印象是,您需要避免频繁的大规模分配。不幸的是,在某些情况下,您无法避免它们,不得不更改代码。正如我所说的,在我的情况下,我改善了切换到std::deque的情况。如果你能辨认出你的记忆碎片,就有可能更准确地谈论它。

这是一个超级简化版的傻瓜。

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

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

这就是所谓的碎片化。

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

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

这是11年来我一直在softwareverify.com上回答人们关于内存碎片问题的答案的高潮

什么样的程序最可能受到影响?

关于内存碎片相关问题的一个很好的(=可怕的)例子是《元素:魔法之战》的开发和发布,这是一款由Stardock开发的电脑游戏。

这款游戏是基于32位/2GB内存构建的,所以必须在内存管理方面进行大量优化,以确保游戏能够在这2GB内存中运行。由于“优化”导致不断分配和取消分配,随着时间的推移,堆内存碎片发生并导致游戏崩溃每一个 时间

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