堆碎片化和Windows内存管理器

8
我在我的程序中遇到了内存碎片问题,无法再一段时间后分配非常大的内存块。我已经阅读了这个论坛上相关的帖子 - 主要是这个。但我仍然有一些问题。
我一直在使用一个内存空间分析器来获取内存的情况。我写了一个包含cin >> var;的1行程序,并对内存进行了截图: alt text 在顶部弧线上 - 绿色表示空闲空间,黄色表示已分配,红色表示已提交。我的问题是右侧的那些已分配内存是什么?它是主线程的堆栈吗?这块内存不会被释放,而且它分裂了我需要的连续内存。在这个简单的1行程序中,分裂并不严重。我的实际程序在地址空间的中间分配了更多的内容,而我不知道它来自哪里。我没有分配那块内存。
2. 我该如何解决这个问题?我正在考虑切换到像nedmalloc或dlmalloc这样的东西。然而,这只适用于我明确分配的对象,而在图片中显示的分裂并不会消失?还是有一种方法可以用另一个内存管理器替换CRT分配?
3. 说到对象,是否有nedmalloc的c++包装器,以便我可以使用new和delete来分配对象?

1
微软安全必应认为原问题中提到的“分析器”应用程序包含Win32.Bisar!rts木马。 - Vinnie Falco
5个回答

12

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

通常,在地址空间的固定点处进行的薄切片是由链接的dll在它们的首选地址处加载引起的。高处加载在地址空间中的dll倾向于是Microsoft操作系统dll。如果所有这些都可以在它们的首选地址处加载,那么操作系统的效率会更高,因为这样dll的只读部分可以在进程之间共享。

您看到的那个切片没什么好担心的,它几乎没有占用您的地址空间。但是,正如您所指出的,还有一些dll加载在地址空间的其他位置。我记得 shlwapi.dll 是一个特别糟糕的例子,加载在大约0x2000000(再次记得)的位置上,这经常将可用地址空间的大部分划分为两个较小的部分。问题在于,一旦加载了DLL,您无法对此分配的空间进行移动。

如果您直接或通过另一个DLL链接到该DLL,则无法做任何事情。如果使用 LoadLibrary ,则可以获取巧妙并保留其首选地址,将其强制重新定位-通常是在地址空间中更好的位置-然后释放该保留内存。不过这并不总是有效。

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

正如您发现的那样,在2GB用户地址空间中很容易用完空间。从根本上说,对内存碎片化的最佳防御措施就是不需要任何大块连续的内存。虽然难以进行反向安装,但将应用程序设计为使用“中等大小”的块通常可以更有效地使用地址空间。

类似地,您可以使用分页策略,可能使用内存映射文件或地址窗口扩展(AWE)


嗨,感谢您提供如此出色的工具,并指出Process Explorer的该功能。 - Budric
@Charles Bailey:关于静态链接- DLL不可以被重新定位吗? - rpg
可以重新排列动态链接库,这样 可以 帮助优化地址空间碎片和加载时间,但是...通常只有您拥有的 DLL 才应该这样做,如果您进行太多微小的优化,最终会得到一组 DLL,在某个时间点上的某个特定 exe 中工作良好,但对于任何其他 exe,其加载策略并不那么好。如果您的 DLL 被重新构建并且大小发生变化,则必须再次执行此过程。因此它在一定程度上可以起作用,但如果您不得不采取此举,可能会陷入高维护恶性循环中。 - CB Bailey

2
我假设您经常分配和释放不同大小的对象,这就是导致内存碎片化问题的原因吗?
有各种策略可以解决这个问题;您提到的不同内存管理器可能会有所帮助,如果它们能够为您解决内存碎片问题,但这需要更深入地分析碎片化的根本原因。例如,如果您经常分配三种或四种类型的对象,并且这些对象 tend to worsen the memory fragmentation issue,那么您可能需要将它们放入自己的内存池中,以便重复使用正确大小的内存块。这样,您应该有一组适合此特定对象的内存块可用,并防止常见情况发生,即 X 对象的分配将足以容纳 Y 的内存块分裂为无法再分配任何 Y。
至于(2),我不知道是否有nedmalloc的包装程序(坦率地说,我对nedmalloc并不十分熟悉),但是您可以非常容易地创建自己的包装程序,因为您可以创建类特定的 new 和 delete 运算符,甚至可以重载/替换全局运算符 new 和 delete。我不是后者的忠实拥护者,但是如果您的分配“热点”由少数几个类组成,通常很容易为它们改装自己的类特定 new 和 delete 运算符。
也就是说,nedmalloc 自称是标准 malloc/free 的替代品,并且至少在 MS 编译器中,我认为 C++ 运行时库将把 new/delete 转发到 malloc/free,因此可能只需要使用 nedmalloc 构建可执行文件。

2
为了减少内存碎片化,您可以利用Windows低碎片堆。我们在产品中使用过这种方法,效果很好,自从使用后,我们几乎没有遇到过内存相关的问题。

我已经看到了这个功能。但是要启用它,您必须运行HeapSetInformation()。该内存快照是在main()的第一行执行时拍摄的,而在前1.3 GB地址空间后,内存已经被分段了。在使用进程资源管理器查看后,发现它是由DLL和其他东西引起的。因此,低碎片堆(LFH)可能有所帮助,但它无法防止由于DLL加载而已经发生的碎片化。 - Budric

1
在程序中查找内存分配位置的最佳方法是使用调试器。每个加载的DLL和可执行文件都有分配,它们会使虚拟内存片段化。此外,使用C/C++库和Windows API将导致在应用程序中创建堆,这至少会保留一块虚拟内存。
例如,您可以使用VirtualAlloc在相对较小的程序中保留大块虚拟内存,只发现VirtualAlloc失败,或者当尝试加载新DLL(等)时应用程序稍后失败。您也不能始终控制将加载哪些DLL以及位置。许多杀毒软件和其他产品将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。


1

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

至于第二点,覆盖全局 new 和 delete 函数非常容易...只需定义它们即可。


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