什么 Alloc API 可以在内部调用 VirtualAlloc/reserve 内存?

What Alloc API may call VirtualAlloc/reserve memory internally?

我正在调试 DLL 中潜在的内存泄漏问题。

案例是处理运行一个子测试,其中loads/unloads一个动态的DLL,在测试期间保留和提交了大量内存(1.3GB)。测试完成并卸载 DLL 后,仍有大量内存保留(1.2GB)。

之所以说这个预留内存是由DLL分配的,是因为如果我使用一个release DLL(没有其他改变,同样的测试),预留内存是~300MB,所以所有额外的预留内存必须在调试中分配动态链接库

看起来在测试期间提交了大量内存,但在测试后取消提交(未释放到空闲状态)。所以我想跟踪谁reserve/decommit那个大内存。但是在源码中,并没有调用VirtualAlloc,所以问题是:

  1. VirtualAlloc 是保留内存的唯一方法吗?
  2. 如果不能,还有什么 API 可以做到这一点?如果是,其他 API 会在内部调用 VirtualAlloc 吗?网上有不少人说HeapAlloc会在内部调用VirtualAlloc?它是如何工作的?

[部分内容纯粹是实现细节,而不是您的应用程序应该依赖的东西,因此仅将它们作为参考目的,而不是作为官方文档或任何形式的合同。也就是说,即使只是为了调试目的,了解事物的底层实现方式还是有一定价值的。]

是的,VirtualAlloc() 函数是 Windows 中内存分配的主力函数。它是一种低级功能,如果您需要它的功能,操作系统会提供给您,但也是系统内部使用的功能。 (准确地说,它可能不直接调用 VirtualAlloc(),而是调用 VirtualAlloc() 也向下调用的更底层函数,如 NtAllocateVirtualMemory(),但这只是语义,不会改变可观察的行为。 )

因此,HeapAlloc() 构建在 VirtualAlloc() 之上,GlobalAlloc() 和 LocalAlloc() 也是如此(尽管后两者在 32 位 Windows 中已过时,并且基本上不应使用通过应用程序——更喜欢显式调用 HeapAlloc()).

当然,HeapAlloc() 不仅仅是 VirtualAlloc() 的简单包装器。它添加了一些自己的逻辑。 VirtualAlloc() 总是以大块的形式分配内存,由系统的分配粒度定义,这是硬件特定的(可通过调用 GetSystemInfo() 并读取 SYSTEM_INFO.dwAllocationGranularity 的值来检索)。 HeapAlloc() 允许您以您需要的任何粒度分配更小的内存块,这更适合典型的应用程序编程。在内部,HeapAlloc() 处理调用 VirtualAlloc() 以获得大块,然后根据需要将其分配。这样不仅更简单API,而且效率更高

请注意,C 运行时库 (CRT) 提供的内存分配函数——即 C 的 malloc() 和 C++ 的 new 运算符——是更高级别的。这些构建在 HeapAlloc() 之上(至少在 Microsoft 的 CRT 实现中)。在内部,他们分配了一个相当大的内存块,基本上用作您的应用程序的 "master" 内存块,然后根据请求将其分成更小的块。当您 free/delete 那些单独的块时,它们将返回到池中。再一次,这个额外的层提供了一个简化的接口(特别是,编写独立于平台的代码的能力),并在一般情况下提高了效率。

内存映射文件和各种 OS APIs 提供的其他功能也建立在虚拟内存子系统之上,因此在内部调用 VirtualAlloc()(或较低级别的等价物) .

所以是的,从根本上说,普通 Windows 应用程序的最低级别内存分配例程是 VirtualAlloc()。但这并不意味着它是您通常应该用于内存分配的主力函数。仅当您确实需要其附加功能时才调用 VirtualAlloc()。否则,要么使用你的标准库的内存分配例程,要么如果你有一些令人信服的理由避免它们(比如不链接到 CRT 或创建你自己的自定义内存池),请调用 HeapAlloc()。

另请注意,您必须始终 free/release 内存使用与您用来分配内存的机制相对应的机制。仅仅因为所有内存分配函数最终都调用 VirtualAlloc() 而不是 就意味着您可以通过调用 VirtualFree() 来释放该内存。如上所述,这些其他函数在 VirtualAlloc() 之上实现附加逻辑,因此需要您调用它们自己的例程来释放内存。如果您通过调用 VirtualAlloc() 自行分配内存,则仅调用 VirtualFree()。如果内存是用 HeapAlloc() 分配的,则调用 HeapFree()。对于 malloc(),调用 free();对于新的,调用删除。


至于你问题中描述的具体场景,我不明白你为什么担心这个。重要的是要记住 reserved 内存和 committed 内存之间的区别。保留只是意味着地址 space 中的这个特定块已保留供进程使用。不能使用保留块。为了使用一块内存,必须提交它,这是指为内存分配后备存储的过程,可以是在页面文件中,也可以是在物理内存中。这有时也称为 mapping。保留和提交可以作为两个单独的步骤完成,也可以同时完成。例如,您可能想保留一个连续地址 space 供将来使用,但实际上您并不需要它,所以您不提交它。已保留但未提交的内存实际上并未分配。

事实上,所有这些保留的内存可能根本就不是泄漏。调试中使用的一种相当常见的策略是保留特定范围的内存地址,而不提交它们,以捕获试图访问此范围内的内存并引发 "access violation" 异常。在发布模式下编译时,您的 DLL 没有进行这些大保留的事实表明,这确实可能是一种调试策略。它还提出了一种更好的确定来源的方法:与其扫描代码以查找所有内存分配例程,不如扫描代码以查找依赖于构建配置的条件代码。如果您在定义 DEBUG_DEBUG 时做一些不同的事情,那么这可能就是魔法发生的地方。

另一种可能的解释是 CRT 对 malloc() 或 new 的实现。当您分配一小块内存(比如几 KB)时,CRT 实际上会保留一个更大的块,但只会提交所请求大小的一块。当您随后 free/delete 那个小块内存时,它将被取消提交,但较大的块不会被释放回 OS。这样做的原因是允许将来调用 malloc/new 以重新使用该保留的内存块。如果后续请求的块大于当前保留地址 space 可以满足的块,它将保留额外的地址 space。如果在调试构建时,您重复分配和释放越来越大的内存块,您看到的可能是内存碎片的结果。但这真的不是问题,除了轻微的性能影响,这在调试构建时真的不值得担心。