内存都去哪了?探究 VirtualAlloc 分配背后被“浪费”的 60KB

摘要

本文记录了一次因 VirtualAlloc 分配失败引发的 OOM 问题排查过程。通过编写测试程序模拟内存分配,发现 32 位进程在未开启 Large Address Aware 时,用户空间仅 2GB 可用,且 VirtualAlloc 实际分配粒度均为 64KB——若申请 4KB,剩余 60KB 将变为不可用空间,导致地址空间碎片化与大量浪费。借助 VMMap 碎片视图与 Windbg 分析,直观展示了分配粒度对内存布局的影响,并验证了按 64KB 对齐分配可避免该问题。

测试程序

测试代码非常简单,我就直接贴到这里了,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "stdafx.h"
#include "stdio.h"
#include "windows.h"

int main(int argc, char *argv[])
{
int nk = 4;
if (argc < 2)
{
printf("usage: TestVirtualAlloc.exe N(kb). default 4kb");
}
else
{
nk = atoi(argv[1]);
}

int idx = 0;
while (true)
{
auto pAddr = VirtualAlloc(nullptr, nk * 1024, MEM_RESERVE, PAGE_READWRITE);
if (pAddr == nullptr)
{
printf("VirtualAlloc(%dkb) loop %6d failed. last error 0n%d\r\n", nk, idx++, GetLastError());
getchar();
}
else
{
printf("VirtualAlloc(%dkb) loop %6d succeed. address 0x%08x\r\n", nk, idx++, pAddr);
}
}
}

Free 空间出乎意料的大

编译 32 位版本的程序,执行 TestVirtualAlloc.exe 4(每次分配 4kb),执行一段时间后,最终会失败,如下图:

virtual-alloc-failed

这是预料内的现象,因为程序在不停的分配空间,32 位进程的虚拟地址空间仅有 4GB,在未开启 Large Address Aware 的情况下,用户程序可用的空间仅有 2GB 空间可用。

windbg 附加到该进程,执行 !address -summary 查看地址空间情况,发现 Free 占比有点太大了(88.90%),这不符合预期。程序在不停的分配空间,虽然没有 commit,但是已经 reserve 了,不应该有这么多 Free 空间才对。

address-summary

VMMap 是查看虚拟内存空间的神器,可以非常详细的查看某个进程的内存空间布局。何不用 VMMap 查看一下?

请出 VMMap

打开 VMMap 并选择 TestVirtualAlloc.exe,查看其虚拟内存空间。

vmmap-view-memory

说明:切记勾选 Options 选项下的 Show Free and Unsuable Regions

当我看到 60 KUnusable 跟在 4 KPrivate Data 后时,沉睡的记忆终于被唤醒了,VirtualAlloc 的分配粒度是 64kb !!!

windbg 中使用 !address 命令查看地址 0x004900000x00491000 的情况,如下图:

windbg-address-490000-491000

地址 0x00491000 所属的区域是 MEM_FREE 的。需要注意的是虽然从 0x00491000 开始的 60kbFree 的,但是这块地址不能被分配使用了,这就是为什么 VMMap 中显示为 Unusable 的原因。

如果我分配的是 64kb,那么就不会有这么大的 Free 空间了。是不是呢?简单验证一下就知道了。

继续验证

执行 TestVirtualAlloc.exe 64(每次会分配 64kb),然后使用 windbg 观察内存空间的情况。

address-summary-no-much-free-memory

可以发现 MEM_FREE 类型的内存空间很小了,基本上全是 MEM_RESERVE 类型的内存空间。使用 VMMap 查看的话,也差不多,这里就不截图了。感兴趣的小伙伴可以自己动手实验。

疑问

折腾完,我突然意识到一个致命问题 —— 我的系统是 64 位的啊。32 位程序在 64 位操作系统下,用户态内存空间应该是 4GB 才对,这里为啥才 2GB 呢?想必聪明的你也一定知道其中的缘由了,想要使用 4GB 的内存空间,必须开启 Large Address Aware 才行。

经过确认,编译程序的时候,默认是没开启这个选项的。

enable-large-address-aware

开启后,再次执行程序,可以发现可分配的内存地址是大于 2gb 的,接近 4gb

allocate-at-address-higher-than-2gb

Fragmentation View

其实,VMMap 还有一个非常强悍的功能,叫 Fragmentation View。此功能可以鸟瞰进程的整个内存空间,也可以放大到对应的区域查看,点击对应区域还可以查看具体的地址范围。从下图可以很明显的看到每次分配 4kb 的内存分布模式 —— 4kbreserve 空间(黄色方块)后跟着 60kbUnusable 空间(灰色区域)。

vmmap-fragment-view

亲自动手

我已经把对应的示例代码上传到了 github,感兴趣的小伙伴可以自行实验。

总结

  • VMMap 是查看虚拟内存空间的神兵利器
  • VirtualAlloc 分配粒度是 64kb,如果分配的过小,会产生浪费,甚至是内存碎片
  • 开启 Large Address Aware 后,32 位程序才能使用更大的内存空间
BianChengNan wechat
扫描左侧二维码关注公众号,扫描右侧二维码加我个人微信:)
0%