堆碎片和 Windows 内存管理器

Posted

技术标签:

【中文标题】堆碎片和 Windows 内存管理器【英文标题】:Heap fragmentation and windows memory manager 【发布时间】:2010-12-13 15:07:08 【问题描述】:

我在程序中遇到内存碎片问题,一段时间后无法分配非常大的内存块。我已经阅读了这个论坛上的相关帖子 - 主要是this 一个。我还有一些问题。

我一直在使用内存空间profiler 来获取内存图片。我写了一个包含 cin >> var; 的 1 行程序。并拍下了回忆:

alt text http://img22.imageshack.us/img22/6808/memoryk.gif 顶部弧线的位置 - 绿色表示空白空间,黄色已分配,红色已提交。我的问题是右边分配的内存是什么?它是主线程的堆栈吗?该内存不会被释放,它会拆分我需要的连续内存。在这个简单的 1 行程序中,拆分并没有那么糟糕。我的实际程序在地址空间的中间分配了更多的东西,我不知道它来自哪里。我还没有分配那个内存。

    我该如何解决这个问题?我正在考虑改用 nedmalloc 或 dlmalloc 之类的东西。但是,这只适用于我自己明确分配的对象,而图片中显示的拆分不会消失?或者有没有办法用另一个内存管理器替换 CRT 分配?

    说到对象,是否有任何用于 c++ 的 nedmalloc 的包装器,以便我可以使用 new 和 delete 来分配对象?

谢谢。

【问题讨论】:

Microsoft Security Essentials 认为原始问题中链接的“分析器”应用程序包含 Win32.Bisar!rts 木马。 【参考方案1】:

为了减少内存碎片,您可以利用Windows Low-Fragmentation Heap。我们在我们的产品中使用了这一点,效果很好,并且自从这样做以来,几乎没有出现与内存相关的问题。

【讨论】:

我见过这个功能。但是,为了启用它,您必须运行 HeapSetInformation()。内存快照是在 main() 的第一行拍摄的,并且在前 1.3 GB 的地址空间之后内存已经碎片化。在进程资源管理器中查看它之后,它是 DLL 和其他东西。所以 LFH 可能会有所帮助,但它并不能防止由于 DLL 加载而已经发生的碎片。【参考方案2】:

首先,感谢您使用我的工具。我希望您觉得它有用并随时提交功能请求或贡献。

通常,地址空间中固定点的薄片是由链接的 dll 在其首选地址加载引起的。在地址空间中加载高的往往是 Microsoft 操作系统 dll。如果这些都可以加载到它们的首选地址,对操作系统来说效率更高,因为这样 dll 的只读部分都可以在进程之间共享。

您可以看到的切片无需担心,它几乎不会从您的地址空间中删除任何内容。正如您所指出的,有一些 dll 会在地址空间的其他点加载。 IIRC shlwapi.dll 是一个特别糟糕的例子,在大约 0x2000000 处加载(同样是 IIRC),这通常会将大部分可用地址空间分成两个较小的部分。这样做的问题是,一旦加载了 DLL,就无法移动这个分配空间。

如果您链接到 DLL(直接或通过另一个 DLL),您将无能为力。如果你使用LoadLibrary,你可以偷偷摸摸地保留它的首选地址,迫使它被重新定位——通常是在地址空间中更好的地方——然后再释放保留的内存。不过,这并不总是有效。

在后台,地址空间监视器使用VirtualQueryEx 来检查进程的地址空间,但是其他工具使用来自 psapi 库的另一个调用(例如Process Explorer)可以显示哪些文件(包括 DLL ) 映射到地址空间的哪些部分。

正如您所发现的,在 2GB 的用户地址空间中用完空间非常容易。从根本上说,您对内存碎片的最佳防御就是不需要任何大的连续内存块。虽然难以改造,但将您的应用程序设计为使用“中等大小”的块通常可以显着提高地址空间的使用效率。

同样,您可以使用分页策略,可能使用内存映射文件或Address Windowing Extensions。

【讨论】:

您好,感谢您提供了一个很棒的工具,并指出了 Process Explorer 的该功能。 @Charles Bailey:重新静态链接 - 不能重新建立 DLL 的基础吗? 是的,您可以重新设置 DLL,这可以帮助优化地址空间碎片和加载时间,但是...通常您应该只对您拥有的 DLL 执行此操作,并且如果你做了太多的微优化,你最终会得到一组 DLL,它们在某个时间点在一个特定的 exe 中运行良好,但对于任何其他 exe,都没有那么好的优化加载策略。如果您的 DLL 被重新构建并更改大小,那么您必须再次执行该过程。所以它可以在一定程度上起作用,但如果你不得不求助于它,你最终可能会陷入高维护的恶性循环。【参考方案3】:

找出程序中内存分配位置的最佳方法是使用调试器。每个加载的 DLL 和可执行文件本身都有分配,所有这些都是虚拟内存碎片。此外,使用 C/C++ 库和 Windows API 会导致在您的应用程序中创建一个堆,这至少会保留一块虚拟内存。

例如,您可以使用 VirtualAlloc 在相对较小的程序中保留一大块虚拟内存,结果却发现 VirtualAlloc 失败,或者应用程序稍后在尝试加载新 DLL 时失败(等等)。也不能总是控制将加载哪些 DLL 以及在何处加载。许多 A/V 和其他产品会在启动时将 DLL 注入到所有正在运行的进程中。发生这种情况时,这些 DLL 通常会首先选择加载地址——也就是说,它们的默认编译/链接可能会被授予。在典型 32 位 Windows 应用程序的可用 2GB 虚拟地址空间中,如果 DLL 在该地址空间的中间加载,那么您可以获得的最大单个分配/保留将少于 1GB。

如果您使用windbg,您可以查看哪些内存区域被消耗、保留等。lm 命令将显示所有DLL 的加载地址和EXE 及其范围。 !vadump 命令将显示进程使用的所有虚拟内存和页面保护。页面保护是对那里的重要提示。例如,在以下(部分)来自 64 位 calc.exe 进程的 !vadump 中,您将看到第一个区域只是一个受保护而无法访问的虚拟内存范围。 (除其他外,这使您无法在地址 0 处分配内存。) MEM_COMMIT 意味着内存由 RAM 或页面文件支持。 PAGE_READWRITE 可能是堆内存,或已加载模块的数据段。 PAGE_READEXECUTE 通常是加载的代码,它将显示在 lm 生成的列表中。 MEM_RESERVE 表示调用 VirtualAlloc 来保留内存区域,但它没有被虚拟内存管理器映射,等等......

0:004> !vadump
BaseAddress:       0000000000000000
RegionSize:        0000000000010000
State:             00010000  MEM_FREE
Protect:           00000001  PAGE_NOACCESS

BaseAddress:       0000000000010000
RegionSize:        0000000000010000
State:             00001000  MEM_COMMIT
Protect:           00000004  PAGE_READWRITE
Type:              00040000  MEM_MAPPED

BaseAddress:       0000000000020000
RegionSize:        0000000000003000
State:             00001000  MEM_COMMIT
Protect:           00000002  PAGE_READONLY
Type:              00040000  MEM_MAPPED

我希望这有助于解释事情。 Windbg 是一个很棒的工具,它有许多扩展来帮助你找到内存的使用位置。

如果您真的只关心堆,请查看 !heap。

【讨论】:

【参考方案4】:

我假设您经常分配和释放不同大小的对象,这就是导致您的内存碎片问题的原因?

有多种策略可以解决这些问题;如果您提到的不同内存管理器可以为您解决碎片问题,它们可能会有所帮助,但这需要对碎片的根本原因进行更多分析。例如,如果您经常分配三种或四种类型的对象,而这些往往会加剧内存碎片问题,您可能希望将它们放入自己的内存池中,以启用正确大小的内存块的重用。那样的话,你应该有一组可用的内存块适合这个特定的对象,并防止对象 X 的分配分裂一个足够大的内存块来容纳 Y 的常见情况,以至于你突然不能分配任何 Y没有了。

至于 (2),我不知道 nedmalloc 的包装器(坦率地说,我对 nedmalloc 不是很熟悉),但是您可以非常轻松地创建自己的包装器,因为您可以创建特定于类的运算符 new并删除甚至重载/替换全局运算符 new 和 delete。我不是后者的忠实拥护者,但如果您的分配“热点”由少数类组成,通常很容易使用自己的、特定于类的运算符 new 和 delete 对其进行改造。

也就是说,nedmalloc 自称是标准 malloc/free 的替代品,至少在 MS 编译器中,我认为 C++ 运行时库会将 new/delete 转发到 malloc/free,因此很可能只是使用 nedmalloc 构建可执行文件。

【讨论】:

【参考方案5】:

它可能是可执行文件吗?它必须被加载到某处的地址空间中......

至于 2,它很容易覆盖全局 new 和 delete 函数......只需定义它们。

【讨论】:

以上是关于堆碎片和 Windows 内存管理器的主要内容,如果未能解决你的问题,请参考以下文章

Objective-C的内存管理

JVM 内存管理

MySQL系列:innodb源代码分析之内存管理

程序中内存从哪里来2之堆内存详解

iOS进程内存分配(页、栈、堆)

malloc函数的原理是啥啊?