如何解决内存碎片问题

49

我们的长时间运行的服务器进程(运行在Windows Server 2003上)会因内存分配失败而抛出异常。我们怀疑这些分配失败是由于内存碎片化引起的。

因此,我们一直在寻找一些替代的内存分配机制,并希望有人告诉我最好的方法:

1)使用Windows 低碎片堆

2)jemalloc-如Firefox 3中使用的

3)Doug Lea的malloc

我们的服务器进程使用跨平台C++代码开发,因此任何解决方案理想情况下都应该是跨平台的(* nix操作系统是否会遭受这种类型的内存碎片化?)。

另外,我是否正确地认为LFH现在是Windows Server 2008 / Vista的默认内存分配机制?如果我们的客户升级他们的服务器操作系统,我的当前问题会“消失”吗?

10个回答

38

首先,我同意其他发帖者提出的可能存在资源泄漏的问题。你需要首先排除这种情况。

希望你所使用的堆管理器有一种方法可以打印出堆中实际可用的总空闲空间(跨所有free块),以及它分成的总块数。如果平均空闲块大小相对于堆中的总空闲空间较小,则表示存在碎片问题。或者,如果你可以打印最大空闲块的大小并将其与总空闲空间进行比较,也可以达到相同的效果。如果运行时遇到了碎片问题,相对于所有块可用的总free空间来说,最大的空闲块会很小。

需要非常明确地指出,以上所有情况都是指堆中的free块,而不是已分配的块。在任何情况下,如果不符合上述条件,则你可能存在某种类型的泄漏情况。

因此,在排除了泄漏问题后,你可以考虑使用更好的分配器。问题中提到的Doug Lea's malloc是一个非常适合一般应用程序的优秀分配器,多数情况下非常健壮。换句话说,它已经经过时间的考验,在大多数应用程序中都能很好地工作。然而,没有任何算法适用于所有应用程序,并且任何管理算法方法都可能会被其设计中的某些恶劣条件打破。

为什么你会遇到碎片问题?——碎片问题的来源是由应用程序的行为引起的,并与在同一内存区域中具有极不同的分配寿命有关。也就是说,一些对象经常分配和释放,而其他类型的对象则在同一堆中持续存在较长时间......可以将寿命较长的对象想象为插入更大区域的洞,从而防止已释放的相邻块合并。

为了解决这种问题,最好的方法是将堆逻辑上分成子区域,使其生命周期更相似。实际上,您需要一个短暂堆和一个持久堆,或者一些将生命周期相似物品分组的堆。 一些人建议采用另一种方法来解决问题,即尝试让分配大小更相似或相同,但这不太理想,因为它会创建一种不同类型的碎片化,称为内部碎片化,这实际上是通过在块中分配比所需空间更多的内存而造成的浪费空间。 此外,使用像Doug Lea的优秀堆分配器这样的工具,使块大小变得更加相似是不必要的,因为分配器已经执行了二的幂大小的桶分配方案,完全不需要人工调整传递给malloc()的分配大小,他的堆管理器可以自动地为您做到这一点,比应用程序能够进行的调整更加强大。

1
我喜欢同时拥有短暂堆和持久堆的想法。这似乎类似于现代垃圾收集器中的“代”概念。这个想法在手动内存分配方案中被广泛实现吗? - Waylon Flinn
@Waylon Flinn - 在嵌入式系统中,创建子堆是解决内存碎片问题的常见方法。另外,许多嵌入式系统尝试通过尽可能多地预分配内存并永远不释放这些长期存在的对象来解决此类问题......实际上是静态分配,至少在某种程度上。如果您有足够的内存可以为系统的整个生命周期专门分配给特定的子系统,那么这不是一个坏的方法。 - Tall Jeff

16

我认为你过早地排除了内存泄漏的可能性。 即使很小的内存泄漏也会导致严重的内存碎片化。

假设你的应用程序表现如下:
分配10MB
分配1个字节
释放10MB
(哎呀,我们没有释放1个字节,但是谁在乎1个微不足道的字节呢)

这似乎是一个非常小的内存泄漏,当仅监视总分配内存大小时,你几乎不会注意到它。 但是,这种泄漏最终会导致应用程序的内存看起来像这样:
.
.
自由 - 10MB
.
.
[已分配-1个字节]
.
.
自由 - 10MB
.
.
[已分配-1个字节]
.
.
自由 - 10MB
.
.

直到你想要分配11MB时,才会注意到这个泄漏...假设你的小内存转储中包含完整的内存信息,我建议使用DebugDiag来查找可能的泄漏。 在生成的内存报告中,仔细检查分配计数(而不是大小)


5
正如您所建议的,Doug Lea的malloc可能效果很好。它是跨平台的,并且已经在实际应用中使用过。至少,将其集成到您的代码中进行测试应该很容易。
在固定内存环境中工作了多年后,即使在非固定环境中,这种情况肯定是一个问题。我们发现CRT分配器在性能方面(速度、浪费空间的效率等)表现得相当糟糕。我坚信,如果您长期需要一个良好的内存分配器,则应编写自己的分配器(或查看类似dlmalloc的东西)。关键是编写出适合您的分配模式的程序,而这与内存管理效率几乎没有任何关系。
试试dlmalloc吧。我绝对推荐它。它也相当可调整,因此您可能可以通过更改一些编译时选项来获得更高的效率。
老实说,您不应该依赖于新的操作系统实现“消失”这种情况。补丁、修补程序或另一个N年后的新操作系统可能会使问题变得更糟。同样,对于需要强大内存管理器的应用程序,请不要使用可用于编译器的标准版本。找到一个适合您情况的版本。从dlmalloc开始,然后进行调整,以查看哪种行为最适合您的情况。

2
您可以通过减少分配和释放的数量来帮助减少碎片化。
例如,对于运行服务器端脚本的 Web 服务器,它可能创建一个字符串以输出页面。而不是为每个页面请求分配和释放这些字符串,只需维护一个池,因此只有在需要更多时才进行分配,但您不会进行释放(这意味着过一段时间后您也不再进行分配,因为您已经有足够的内存)。
您可以使用 _CrtDumpMemoryLeaks(); 在运行调试版本时将内存泄漏转储到调试窗口中,但我认为这仅适用于 Visual C 编译器。(它在 crtdbg.h 中)

1

在怀疑碎片化之前,我会怀疑内存泄漏。

对于内存密集型的数据结构,您可以切换到可重用的存储池机制。您还可以将更多的内容分配到堆栈上,而不是堆上,但在实际应用中,我认为这不会有太大的区别。

我会启动像valgrind这样的工具或进行一些密集的日志记录来查找未被释放的资源。


1
@nsaners - 我相当确定问题是由于内存碎片化引起的。我们已经分析了指向在分配大块(5-10mb)内存时出现问题的minidumps。我们还监控了进程(现场和开发中)以检查内存泄漏,但没有检测到任何问题(内存占用通常很低)。

2
你在这里得出结论时必须小心。即使是非常小的泄漏也会导致堆的大规模碎片化。想象一下每个未被释放的1字节泄漏malloc()就像堆中的弹孔。相邻的块永远无法合并。 - Tall Jeff

1

虽然通常情况下Unix上的问题不会那么严重,但这个问题确实存在。

低碎片堆对我们有所帮助,但我的同事们却很喜欢Smart Heap(它已经在我们的几个产品中跨平台使用多年了)。不幸的是,由于其他情况,这次我们不能使用Smart Heap。

我们还研究了块/分配和尝试拥有范围明智的池/策略,即长期的东西在这里,整个请求的东西在那里,短期的东西在那里等等。


1
通常情况下,你可以通过浪费一些内存来获得一些速度。
这种技术对于通用分配器来说并不实用,但是它确实有它的用处。
基本上,想法是编写一个分配器,该分配器从池中返回所有分配大小相同的内存。这个池永远不会变得碎片化,因为任何块都和另一个一样好。您可以通过创建具有不同大小块的多个池并选择最小的块大小池来减少内存浪费,而这个池仍然大于所请求的数量。我使用了这个想法来创建运行在O(1)时间复杂度下的分配器。

我不同意这个想法。增加所有分配的大小会创建内部碎片。如果你可以浪费内存,最好只增加堆的总大小 - 这本身可能避免了碎片化成为问题。即:堆的不稳定完全被避免了。 - Tall Jeff

-1

简单、快速且不太优雅的解决方案是将应用程序分成几个进程,每次创建进程时都应该获得新的HEAP。

您的内存和速度可能会受到一些影响(交换),但快速的硬件和大容量的RAM应该能够提供帮助。

这是旧版UNIX守护进程的技巧,在线程尚未存在时使用。


-1

如果你在谈论Win32 - 你可以尝试使用LARGEADDRESSAWARE来挤压一些东西。这样你就会有大约1GB的额外碎片化内存,因此你的应用程序将更长时间地进行碎片化。


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