243

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

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

还:

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

12 回答 12

350

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

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

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

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

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

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

现在,尝试分配 16 个字节。哎呀,我不能,即使有几乎两倍的免费。

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

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

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

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

内存碎片的典型症状是您尝试分配一个大块并且您不能,即使您似乎有足够的可用内存。另一个可能的后果是进程无法将内存释放回操作系统(因为它从操作系统分配的每个大块,用于malloc细分等等,都留下了一些东西,即使每个块的大部分现在未使用)。

在 C++ 中防止内存碎片的策略是根据对象的大小和/或预期的生命周期从不同的区域分配对象。因此,如果您要创建大量对象并稍后将它们全部销毁,请从内存池中分配它们。您在它们之间进行的任何其他分配都不会来自池,因此不会位于它们之间的内存中,因此内存不会因此而碎片化。或者,如果您要分配大量相同大小的对象,则从同一个池中分配它们。然后,池中的一段可用空间永远不会小于您尝试从该池分配的大小。

通常你不需要太担心它,除非你的程序是长时间运行的并且做了很多分配和释放。当您同时拥有短寿命和长寿命物体时,您的风险最大,但即便如此,您malloc也会尽最大努力提供帮助。基本上,忽略它,直到您的程序分配失败或意外导致系统内存不足(在测试中抓住这一点,优先考虑!)。

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

于 2010-09-22T15:02:52.050 回答
93

什么是内存碎片?

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

考虑这一点的最简单方法是想象你有一面大的空墙,你需要在上面放置不同尺寸的图片。每张图片都占据一定的尺寸,您显然无法将其拆分成更小的部分以使其适合。你需要在墙上有一个空的地方,图片的大小,否则你不能把它放上去。现在,如果你开始把照片挂在墙上,而你不小心如何安排它们,你很快就会得到一堵部分被照片覆盖的墙,即使你可能有空白点,但大多数新照片都不适合因为它们比可用的位置大。您仍然可以挂非常小的图片,但大多数图片不适合。所以你必须重新排列(紧凑)已经在墙上的那些,以便为更多空间腾出空间。

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

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

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

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

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

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

于 2010-09-22T15:00:27.807 回答
26

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

假设有一个简单的玩具示例,您有 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,我们不能自动找到并更改所有这些值。

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

在这个例子中,3 个字节并不是很多浪费,但是考虑一个更病态的情况,例如两个字节的两个分配在内存中相距十兆字节,并且您需要分配一个大小为 10 兆字节的块+ 1 个字节。你必须去向操作系统请求超过十兆字节的虚拟内存才能做到这一点,即使你已经拥有足够的空间只差一个字节。

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

通常,您执行的分配越少,内存碎片的可能性就越小。然而,STL 相当有效地处理了这个问题。如果您有一个字符串正在使用其当前分配的全部内容并且您将一个字符附加到它,它不会简单地重新分配到其当前长度加一,而是将其长度加倍。这是“频繁小分配池”策略的变体。该字符串正在占用一大块内存,以便它可以有效地处理大小重复的小幅增加,而无需进行重复的小幅重新分配。实际上所有的 STL 容器都在做这种事情,所以通常你不需要太担心由自动重新分配 STL 容器引起的碎片。

虽然当然 STL 容器不会在彼此之间共享内存,所以如果您要创建许多小容器(而不是一些经常调整大小的容器),您可能必须像您一样关注防止碎片化适用于任何频繁创建的小对象,无论是否 STL。

于 2010-09-22T15:10:20.343 回答
14
  • 什么是内存碎片?

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

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

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

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

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

  • Paul R. Wilson、Mark S. Johnstone、Michael Neely 和 David Boles。动态存储分配:调查和严格审查。在 1995 年记忆管理国际研讨会论文集上,Springer Verlag LNCS,1995
  • 马克·S·约翰斯通、保罗·R·威尔逊。内存碎片问题:解决了吗?在 ACM SIG-PLAN Notices,第 34 卷第 3 期,第 26-36 页,1999 年
  • 加里先生、RL 格雷厄姆和 JD 厄尔曼。内存分配算法的最坏情况分析。在第四届 ACM 计算理论年度研讨会上,1972 年
于 2010-09-22T15:04:25.823 回答
10

更新:
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 字符串,但正如我从运行中看到的那样,info heap它们不会导致任何问题)。我的印象是你需要避免频繁的大分配。不幸的是,在某些情况下您无法避免它们并且必须更改您的代码。正如我所说的,我在切换到std::deque. 如果您确定了您的内存碎片,则可以更准确地谈论它。

于 2010-09-22T15:46:53.750 回答
6

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

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

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

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

于 2010-09-22T15:01:56.107 回答
6

可以在此处找到有关内存碎片的非常详细的答案。

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

这是我在 softwareverify.com 上向人们询问有关内存碎片问题的 11 年内存碎片答案的结晶

于 2015-10-12T23:25:45.610 回答
3

什么是内存碎片?

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

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

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

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

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

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

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

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

于 2010-09-22T15:02:11.777 回答
3

这是一个超级简化的假人版本。

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

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

这就是所谓的碎片化。

于 2010-09-22T16:27:57.217 回答
2

当您想在堆上添加一个项目时,计算机必须搜索空间以容纳该项目。这就是为什么没有在内存池或池分配器上进行动态分配可以“减慢”事情的原因。对于繁重的 STL 应用程序,如果您正在执行多线程,则可以使用Hoard 分配器TBB Intel版本。

现在,当内存碎片化时,可能会发生两种情况:

  1. 必须进行更多搜索才能找到粘贴“大”物体的好空间。也就是说,在某些情况下,由于许多小对象分散在寻找一块好的连续内存块可能是困难的(这些是极端的。)
  2. 记忆不是一些容易阅读的实体。处理器受限于它们可以容纳的数量和位置。如果他们需要的项目在一个地方但当前地址在另一个地方,他们会通过交换页面来做到这一点。如果您经常需要交换页面,则处理速度可能会减慢(同样,这会影响性能的极端情况。)请参阅虚拟内存上的此帖子。
于 2010-09-22T15:03:51.713 回答
1

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

本质上,在大多数系统上,内存仅以一定大小的块形式出现。一旦你将这些块分开,它们就不能重新加入,直到整个块被释放。当实际上只有一小部分块在使用时,这可能会导致整个块在使用中。

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

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

于 2010-09-22T15:09:28.750 回答
0

什么样的程序最容易受到影响?

与内存碎片相关的问题的一个很好(=可怕)的例子是Stardock的电脑游戏“元素:魔法之战”的开发和发布。

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

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

于 2020-06-03T23:08:06.250 回答