32位程序突破4GB内存限制:大地址标志、AWE与文件映射实战

发布时间:2026/11/6 2:13:35
32位程序突破4GB内存限制:大地址标志、AWE与文件映射实战

简介:面向仍在维护32位C++/C#程序的开发者,聚焦默认约2GB用户态地址空间限制与Large Address Awareness(LAA)相关处理思路,帮助需要处理大数据、图像或游戏等内存密集任务的读者理解如何让32位程序按需申请更大地址空间,也适合评估遗留工程内存瓶颈与兼容性风险的中级技术人员参考。压缩包共164个文件,以112个dll和40个exe为主,另含config、sys、txt、xml等配置与说明文件,整体约34.37MB,可对照工具链文件、运行配置和程序集进行排查。已有1270人学习下载。读者可借助包内可执行文件、动态库与配置项,梳理LAA设置、编译器与链接器工具配置及兼容性检查的参考线索,为优化32位程序内存使用或后续迁移64位提供依据。

1. 32位程序申请4GB内存:先分清“看到”和“拿到”

某图像处理Demo在64位机器上处理一张大图,任务管理器显示物理内存还有富余,程序却在VirtualAlloc返回NULL后崩掉。日志里的提交内存停在2.1GB附近,很多人第一反应是加内存条,真正卡住的是32位程序的地址空间账本。这个标题要解决的不是“买多少内存”,而是让32位程序在64位系统上把用户态地址空间从2GB扩到接近4GB,并在需要更多物理内存时用AWE或分块映射绕开单进程上限。适合还在维护32位C/C++程序、被大块分配失败折磨的从业者;如果程序必须一次性持有超过4GB连续缓冲区,趁早评估64位版本,别在32位里硬扛。

2. 地址空间账本:为什么32位程序默认只敢用2GB

2.1 2GB、3GB、4GB三种地址空间模型

32位指针宽度是4字节,理论寻址范围是0到0xFFFFFFFF,也就是4GB。但“能寻址4GB”和“进程能用到4GB”是两件事。32位系统默认把低2GB给用户态,高2GB留给内核,用户程序即使有4GB指针,也只能在低2GB里活动。后来出现了/3GB启动开关,把用户态抬到3GB、内核压到1GB,代价是内核空间变紧,某些驱动和缓存会不舒服。再往后,64位系统普及,32位进程通过WOW64运行在64位内核上,内核不再占用这32位进程的低4GB,所以只要PE头里带一个大地址标志,用户态就能摸到接近4GB的地址空间。

这里最容易混淆的是/PAE。PAE是物理地址扩展,让32位系统能访问超过4GB的物理内存,但单个32位进程的虚拟地址空间仍然受4GB指针限制。它解决的是“系统能插更多内存条”,不是“单进程能一次拿到4GB”。如果程序要的是单进程大缓冲区,PAE帮不上核心忙;如果程序要的是多个进程分摊大内存,PAE才有意义。

在64位系统上跑32位程序,情况又不一样。WOW64层会把32位进程的地址空间单独摆出来,默认仍然是2GB用户态,除非可执行文件声明自己认识大地址。声明之后,用户态上限可以到0xFFFEFFFF附近,实际可用通常略低于4GB,因为可执行文件、系统DLL、线程栈、TEB/PEB、堆管理结构都要占地方。我一般把“接近4GB”理解成3.5GB到3.8GB的可用区间,具体看加载了多少DLL、开了多少线程、堆预留多大。

提示:排查时先确认进程跑在32位系统还是64位系统上。64位系统上的32位进程,先看PE头大地址标志;32位系统才去看/3GB启动开关。顺序反了容易白折腾。

2.2 /LARGEADDRESSAWARE改的是PE头哪一位

PE文件里有一个COFF File Header,里面有个Characteristics字段,宽度2字节。其中0x0020这一位叫IMAGE_FILE_LARGE_ADDRESS_AWARE,链接器选项/LARGEADDRESSAWARE干的事就是把它置上。这个标志不是“强行给程序4GB”,而是告诉加载器:这个程序能处理大于0x7FFFFFFF的地址,别把它按2GB边界来限制。加载器看到标志后,在64位系统上会把用户态上限放开;在32位系统上,还要配合/3GB启动开关,才能把用户态从2GB抬到3GB。

标志位置不需要死记,用Python读PE头就能看到。先读DOS头的e_lfanew,跳到PE签名,再往后4字节是COFF头起点,Characteristics在COFF头偏移18处。代码不复杂,后面第3章会给一个不依赖第三方库的校验脚本。关键点是:这个位只影响“进程地址空间上限”,不影响物理内存够不够,也不保证大块连续内存一定分配成功。

如果主程序加了标志,但某个旧DLL没加,会不会把主程序拖回2GB?严格说,进程地址空间上限由主EXE的标志和系统环境决定,DLL的标志不会直接把进程降回2GB。但旧DLL里如果有“指针最高位是符号位”的假设,或者用有符号整数比较地址,就可能在大地址下翻车。这类问题不是标志本身能修的,只能给DLL也加标志并回归,或者把旧DLL隔离到独立进程。

2.3 用GetSystemInfo和PE头字段核对当前进程上限

动手前先写一个最小探针,确认当前进程看到的用户态上限。下面这段C++在32位编译下运行,打印GetSystemInfo返回的地址范围,并尝试预留2GB、3GB地址空间。MEM_RESERVE只占地址空间,不提交物理页,适合看地址空间账本;如果连预留都失败,说明上限或碎片已经卡住。

#include <windows.h> #include <cstdio> int main() { SYSTEM_INFO si; GetSystemInfo(&si); printf("wow64 min=%p max=%p page=%lu\n", si.lpMinimumApplicationAddress, si.lpMaximumApplicationAddress, si.dwPageSize); SYSTEM_INFO nsi; GetNativeSystemInfo(&nsi); printf("native arch=%u page=%lu\n", nsi.wProcessorArchitecture, nsi.dwPageSize); for (SIZE_T gb = 2; gb <= 3; ++gb) { SIZE_T bytes = (SIZE_T)gb * 1024ULL * 1024ULL * 1024ULL; void* p = VirtualAlloc(NULL, bytes, MEM_RESERVE, PAGE_NOACCESS); printf("reserve %luGB -> %p err=%lu\n", (unsigned long)gb, p, GetLastError()); if (p) { VirtualFree(p, 0, MEM_RELEASE); } } return 0; }

逻辑说明:GetSystemInfo在WOW64进程里返回的是32位视图,lpMaximumApplicationAddress能看出当前用户态上限;GetNativeSystemInfo返回底层系统架构,用来确认是不是64位系统。VirtualAlloc的MEM_RESERVE只保留地址区间,不占用物理页,所以适合做地址空间探针。参数PAGE_NOACCESS表示先不给访问权限,后面真正使用时再VirtualAlloc提交或VirtualProtect改权限。

运行结果怎么读:没打大地址标志时,max通常在0x7FFEFFFF附近,预留3GB会失败;打了标志且在64位系统上,max会到0xFFFEFFFF附近,预留3GB有机会成功,但2GB到3GB之间可能因为DLL和堆的占用出现空洞。探针不要等到业务跑完再跑,那时候地址空间已经被切碎,结论会被带偏。

3. 给32位EXE打上大地址标志:链接期和发布后两种做法

3.1 链接器加/LARGEADDRESSAWARE的最小配置

有源码时最干净的做法是在链接阶段加标志。不同构建系统写法不一样,但本质都是把IMAGE_FILE_LARGE_ADDRESS_AWARE写进PE头。下面给三种常见写法,命令里的app.exe换成自己的目标名。

# 直接调用链接器时 link /LARGEADDRESSAWARE /OUT:app.exe main.obj user32.lib # CMake 里给某个目标加链接选项 target_link_options(app PRIVATE /LARGEADDRESSAWARE) # GCC 工具链下用链接器透传参数 gcc main.c -o app.exe -Wl,--large-address-aware

逻辑说明:/LARGEADDRESSAWARE是链接器选项,不是编译器选项,所以放在链接命令行或target_link_options里。/OUT指定输出文件,后面跟依赖库。GCC工具链下由-Wl,把参数透传给链接器,--large-address-aware是链接器认识的等价开关。加完后重新链接,不要只改构建脚本不清理旧产物,否则跑的还是旧EXE。

参数上要注意架构。32位目标才需要这个标志;64位目标本身地址空间足够,加不加不影响核心问题。混合构建里经常出现“主程序32位、某个静态库64位”的配置错误,那种情况链接阶段就会报错,轮不到地址空间问题。还有一种情况是程序用/FIXED固定基址加载,加载器可选空间变小,大地址标志虽然生效,但地址空间碎片会更明显。我一般建议32位大内存程序不要固定基址,让加载器有回旋余地。

注意:改链接选项后要重新做一遍完整回归。大地址标志会把DLL加载到2GB以上,旧代码里如果用了有符号指针比较、把地址强转成int、或者用0x80000000做哨兵值,问题会在高地址下暴露出来。

3.2 没有源码时用editbin改PE头

第三方EXE或老项目没有源码,可以用链接器自带的editbin工具直接改PE头。操作前先备份,改完用校验脚本确认标志位。命令很短,但风险不在命令本身,而在改完之后程序行为变化。

# 先备份,别省这一步 copy app.exe app.exe.bak # 给EXE打上大地址感知标志 editbin /LARGEADDRESSAWARE app.exe # 查看PE头里的标志状态,确认是否生效 dumpbin /headers app.exe | findstr /i "large"

逻辑说明:editbin /LARGEADDRESSAWARE只改PE头的Characteristics字段,不重写代码段。dumpbin /headers会输出Application can handle large (>2GB) addresses之类的行,findstr用来过滤。改完后如果程序有数字签名,签名会失效,分发前要重新签;如果是受保护或加壳的EXE,editbin可能改不动或者改完跑不起来,这种就不要硬改,找原厂或走独立进程方案。

发布后改标志的适用场景是“程序能跑,只是卡在2GB”。如果程序本身有指针假设问题,改完可能从“稳定地失败”变成“偶发地崩”,更难查。我的做法是先在测试机改一个副本,跑一轮大内存压测,确认没有高地址访问冲突,再动生产包。DLL也要逐个看,尤其是老式图像编解码、数据库客户端、硬件SDK这类依赖。

3.3 用Python脚本校验Characteristics字段

不装第三方库也能校验PE头。下面脚本读DOS头、PE签名、COFF头,取Characteristics第0x0020位。可以一次传多个文件,批量看EXE和DLL。

import struct import sys IMAGE_FILE_LARGE_ADDRESS_AWARE = 0x0020 def read_pe_flags(path): with open(path, "rb") as f: data = f.read() if len(data) < 0x40 or data[:2] != b"MZ": return None e_lfanew = struct.unpack_from("<I", data, 0x3C)[0] if data[e_lfanew:e_lfanew + 4] != b"PE\0\0": return None coff = e_lfanew + 4 characteristics = struct.unpack_from("<H", data, coff + 18)[0] return characteristics if __name__ == "__main__": for path in sys.argv[1:]: flags = read_pe_flags(path) if flags is None: print(f"{path}: not a valid PE file") continue laa = bool(flags & IMAGE_FILE_LARGE_ADDRESS_AWARE) print(f"{path}: LAA={laa} Characteristics=0x{flags:04X}")

逻辑说明:data[:2] == b"MZ"确认DOS头,0x3C处是e_lfanew,指向PE签名。PE签名占4字节,后面紧跟COFF头,Characteristics在COFF头偏移18。struct.unpack_from("<H", ...)按小端读2字节。参数就是文件路径,可以传多个。输出LAA=True表示大地址标志已置位。

这个脚本只读不写。手改PE头容易漏掉校验和、签名、节表对齐,出了问题很难回滚。要改就用editbin,改完再用脚本批量核对。批量场景下我一般把主EXE和依赖DLL列一个清单,逐个跑脚本,避免“主程序改了、某个DLL没改”的漏网。

4. 真正超过4GB:AWE窗口和分块内存池怎么落地

4.1 AWE三步法:分配物理页、预留窗口、映射

大地址标志把32位进程的用户态上限抬到接近4GB,但单进程虚拟地址空间的天花板还在。如果程序要用的物理内存超过4GB,比如一个图像缓存池要放6GB数据,靠VirtualAlloc不够,得用AWE。AWE的思路是:先向系统申请一批物理页并锁定,再在32位地址空间里预留一个窗口,把物理页映射进窗口使用。窗口大小受限,但背后的物理页可以超过4GB。

AWE需要SeLockMemoryPrivilege权限,普通用户默认没有,通常要管理员运行,或者在本地安全策略里给账户授权。代码分三步:启用锁页权限、AllocateUserPhysicalPages分配物理页、VirtualAlloc预留MEM_PHYSICAL窗口、MapUserPhysicalPages映射。下面是最小核心片段。

#include <windows.h> #include <vector> #include <cstdio> bool EnableLockPages() { HANDLE token = NULL; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token)) { return false; } TOKEN_PRIVILEGES tp = {}; tp.PrivilegeCount = 1; LookupPrivilegeValueW(NULL, SE_LOCK_MEMORY_NAME, &tp.Privileges[0].Luid); tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; BOOL ok = AdjustTokenPrivileges(token, FALSE, &tp, sizeof(tp), NULL, NULL); DWORD err = GetLastError(); CloseHandle(token); return ok && err == ERROR_SUCCESS; } int main() { if (!EnableLockPages()) { printf("enable lock pages failed: %lu\n", GetLastError()); return 1; } ULONG_PTR pages = 256 * 1024; // 1GB / 4KB std::vector<ULONG_PTR> phys(pages); if (!AllocateUserPhysicalPages(GetCurrentProcess(), &pages, phys.data())) { printf("alloc physical failed: %lu\n", GetLastError()); return 1; } SIZE_T windowBytes = pages * 4096ULL; void* window = VirtualAlloc(NULL, windowBytes, MEM_RESERVE | MEM_PHYSICAL, PAGE_READWRITE); if (!window) { printf("reserve window failed: %lu\n", GetLastError()); FreeUserPhysicalPages(GetCurrentProcess(), &pages, phys.data()); return 1; } if (!MapUserPhysicalPages(window, pages, phys.data())) { printf("map failed: %lu\n", GetLastError()); VirtualFree(window, 0, MEM_RELEASE); FreeUserPhysicalPages(GetCurrentProcess(), &pages, phys.data()); return 1; } // 这里可以像普通内存一样读写 window,大小是 windowBytes printf("AWE window=%p bytes=%llu\n", window, (unsigned long long)windowBytes); MapUserPhysicalPages(window, pages, NULL); // 解除映射 FreeUserPhysicalPages(GetCurrentProcess(), &pages, phys.data()); VirtualFree(window, 0, MEM_RELEASE); return 0; }

逻辑说明:EnableLockPages打开当前进程令牌,查SE_LOCK_MEMORY_NAME并启用。AllocateUserPhysicalPages的pages是输入输出参数,传入想要的页数,返回实际分配页数;phys数组保存物理页帧号。VirtualAlloc用MEM_RESERVE | MEM_PHYSICAL预留地址窗口,PAGE_READWRITE给读写权限。MapUserPhysicalPages把物理页映射到窗口,之后window就能像普通指针一样访问。最后用MapUserPhysicalPages(window, pages, NULL)解除映射,再释放物理页和窗口。

参数上,页大小通常是4KB,pages不要一次要太满,先按目标内存的70%到80%估。AWE锁定的页不能被换出,分配太多会让系统内存压力变大,甚至影响其他进程。MapUserPhysicalPages一次映射的窗口不能超过预留大小,窗口用完后可以换一批物理页映射进来,这就是“窗口滚动”的用法。错误码1314表示权限不足,先查锁页权限,不要怀疑API。

4.2 分块内存池:CreateFileMapping+MapViewOfFile

如果数据本身是文件,或者可以落到磁盘,分块内存池比AWE更省心。CreateFileMapping创建一个大的映射对象,大小可以超过4GB;MapViewOfFile按需把某一段映射进32位地址空间,用完UnmapViewOfFile。同一时刻地址空间里只放当前要处理的一块,背后的文件可以很大。

#include <windows.h> #include <cstdio> int main() { unsigned long long total = 6ULL * 1024 * 1024 * 1024; // 6GB HANDLE hMap = CreateFileMappingW( INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, (DWORD)(total >> 32), (DWORD)total, L"bigpool"); if (!hMap) { printf("create mapping failed: %lu\n", GetLastError()); return 1; } unsigned long long offset = 0; SIZE_T chunk = 512ULL * 1024 * 1024; // 每次映射512MB void* view = MapViewOfFile( hMap, FILE_MAP_ALL_ACCESS, (DWORD)(offset >> 32), (DWORD)offset, chunk); if (!view) { printf("map view failed: %lu\n", GetLastError()); CloseHandle(hMap); return 1; } // 使用 view,长度 chunk printf("view=%p offset=%llu chunk=%llu\n", view, offset, (unsigned long long)chunk); UnmapViewOfFile(view); CloseHandle(hMap); return 0; }

逻辑说明:CreateFileMappingW的第一个参数用INVALID_HANDLE_VALUE表示匿名映射,不落具体文件;也可以用真实文件句柄做持久化。大小拆成高32位和低32位传入,total >> 32是高32位,(DWORD)total是低32位。MapViewOfFile的偏移也拆成高/低32位,chunk是本次视图大小。视图大小不要超过当前可用地址空间,32位进程里一次映射1GB以上就要小心碎片。

参数上,chunk建议按业务块大小设,比如图像按行块、数据库按页块。映射偏移要按系统分配粒度对齐,常见是64KB。FILE_MAP_ALL_ACCESS需要映射对象有读写权限;只读场景用FILE_MAP_READ。用完一定UnmapViewOfFile,否则地址空间会被视图占住,映射多了照样失败。文件映射的页可以由系统换出,物理内存压力比AWE小,但磁盘I/O和缺页会成为新瓶颈。

4.3 AWE、文件映射、分块堆的选型参数表

三条路都能让32位程序碰超过4GB的数据,但代价不同。下面这张表是我选型时先填的几列,填完基本就知道该走哪条。

方案能否超过4GB是否需要特殊权限是否可换出适合场景主要风险
/LARGEADDRESSAWARE单进程虚拟地址接近4GB否是原有32位程序想多用地址空间高地址假设导致旧代码崩
AWE物理页可超过4GB需要锁页权限否,锁定物理页大缓存、数据库缓冲池权限、内存压力、窗口切换复杂
文件映射映射对象可超过4GB否是大文件、可分块处理的数据缺页I/O、视图管理
分块堆总量受物理内存和地址空间限制否是可拆块的结构体、数组碎片、跨块指针不能直接用

选型时先问三个问题:数据能不能拆块?能不能落盘?能不能接受锁页?能拆块且能落盘,优先文件映射;不能落盘但能拆块,用分块堆加AWE;必须一次访问大块且不能换出,才考虑AWE。大地址标志是基础项,不是替代项。很多程序加了标志就以为万事大吉,结果最大连续块还是只有几百MB,那就是碎片问题,不是标志问题。

5. 避坑与排查:32位大内存方案的五个翻车现场

5.1 加了标志仍申请大块失败:碎片和保留区

现象:EXE已经用editbin打了大地址标志,GetSystemInfo也显示上限到0xFFFEFFFF,但VirtualAlloc申请1.5GB还是返回NULL。任务管理器里提交内存并不高,看起来“明明有空间”。

原因:地址空间上限放开不等于连续空间足够。进程启动后,EXE、系统DLL、CRT、线程栈、堆管理结构会把地址空间切成多段,2GB以上可能被DLL占掉一块,低地址又有保留区。VirtualAlloc要求连续区间,最大连续块往往远小于总可用空间。程序如果先跑了一堆小分配,再申请大块,碎片会更严重。

解决:把大块预留提前到进程启动早期,在加载业务DLL和创建大量线程之前做。能拆块就拆块,不要追求单块1GB以上。用VirtualQuery遍历地址空间,打印空闲区间,确认最大的空洞在哪里。实在需要大连续块,考虑AWE窗口或文件映射,把“连续”需求降下来。

5.2 第三方DLL没打标志,主程序被拖回2GB

现象:主程序加了大地址标志,单独跑正常;加载某个旧DLL后,偶尔在访问高地址指针时崩溃,或者某些分配突然失败。换回旧版主程序(不加标志)反而稳定。

原因:主程序标志本身不会因为DLL没标志就失效,但旧DLL里可能有地址假设。比如把指针强转成int做比较,用0x80000000当非法值,或者用有符号减法算距离。这些代码在2GB以下碰巧成立,DLL被加载到高地址后就翻车。这类问题属于“血泪经验”型坑,日志里通常只看到访问冲突,看不出根因。

解决:给依赖DLL也补大地址标志,逐个回归。不能改的DLL,尝试把它隔离到独立进程,通过IPC传数据。还可以用加载器策略限制某些DLL的加载顺序,但这不是长久办法。上线前做一轮高地址压力测试,专门让分配器把DLL和堆推到2GB以上,观察旧模块行为。

5.3 AWE报1314:锁页权限没启用

现象:AllocateUserPhysicalPages返回失败,GetLastError()是1314,换成管理员运行有时能过,有时还是不行。代码检查过参数,页数也不大。

原因:1314是ERROR_PRIVILEGE_NOT_HELD,当前进程令牌没有启用SeLockMemoryPrivilege。管理员账户也不等于进程自动拥有这个权限,需要显式启用,或者账户在本地策略里被授予“锁定内存页”。有些环境把策略收紧,管理员也拿不到。

解决:先调用OpenProcessToken和AdjustTokenPrivileges启用权限,检查GetLastError()在AdjustTokenPrivileges之后是否还是ERROR_SUCCESS。如果启用失败,去本地安全策略里给运行账户加“锁定内存页”,重启会话再试。不要用AllocateUserPhysicalPages反复重试代替权限检查,错误码已经说得很清楚。

5.4 32位系统开/3GB后驱动异常

现象:为了给32位程序腾出3GB用户态,在启动配置里加了/3GB,程序确实能多申请一些内存,但某个硬件驱动开始报错,甚至系统蓝屏。回退启动开关后恢复。

原因:/3GB把内核空间从2GB压到1GB,某些驱动需要大块连续内核内存,或者假设内核地址空间有2GB。驱动不是普通应用,压内核空间会直接影响它的可用资源。再加上/PAE、/NX等开关组合,兼容性问题更复杂。

解决:生产环境不要为了单个32位程序随便开/3GB。能上64位系统就上64位系统,32位进程在WOW64下加大地址标志更干净。必须留在32位系统时,先在隔离环境验证所有驱动和内核模块,确认没有内核大块分配需求。回退方案要提前写好,别等蓝屏了再找启动配置。

5.5 提交内存和物理内存混为一谈

现象:任务管理器显示物理内存用了60%,程序却VirtualAlloc失败;或者物理内存快满了,程序反而还能分配。看资源监视器里的“提交大小”和“工作集”对不上,排查方向被带偏。

原因:VirtualAlloc提交失败可能受提交限制、页面文件大小、地址空间碎片影响,不只是物理内存。Windows的提交限制是物理内存加页面文件能支撑的虚拟提交总量,程序提交了但没访问的页可以被换出,物理内存不一定马上涨。反过来,物理内存紧张时系统会换页,程序可能变慢但不一定分配失败。

解决:排查时同时看MEMORYSTATUSEX里的ullTotalVirtual、ullAvailVirtual、ullTotalPageFile、ullAvailPageFile,再配合GetPerformanceInfo看提交总量。区分MEM_RESERVE和MEM_COMMIT:预留失败是地址空间问题,提交失败更可能是提交限制或页面文件问题。日志里把GetLastError()和当前提交值一起打出来,别只写“内存不足”。

6. 用地址空间探针压出真实上限:我常用的三个验证动作

大地址标志、AWE、文件映射都配好之后,别急着上业务。我一般先跑一个地址空间探针,把“理论上限”压成“这台机器上的真实上限”。探针放在进程启动后第一分钟,先于UI初始化和业务线程,避免碎片干扰。

第一个动作是预留探针。从64MB开始倍增到2GB,每次VirtualAlloc(NULL, size, MEM_RESERVE, PAGE_NOACCESS),记录成功最大值和失败错误码。第二个动作是提交探针,对最大成功预留块做MEM_COMMIT,看提交限制在哪里。第三个动作是AWE窗口滚动,分配目标物理页后,用不同窗口大小映射,观察MapUserPhysicalPages的耗时和失败点。

#include <windows.h> #include <cstdio> void probe_reserve() { for (SIZE_T mb = 64; mb <= 2048; mb *= 2) { SIZE_T bytes = mb * 1024ULL * 1024ULL; void* p = VirtualAlloc(NULL, bytes, MEM_RESERVE, PAGE_NOACCESS); printf("reserve %4lu MB -> %p err=%lu\n", (unsigned long)mb, p, GetLastError()); if (p) VirtualFree(p, 0, MEM_RELEASE); } }

这张表是我压测时填的,填完再决定业务块大小。

测试项观察值判断
lpMaximumApplicationAddress0xFFFEFFFF附近大地址标志已生效
最大预留块小于总可用空间存在碎片或DLL占用
最大提交块受页面文件和提交限制需要调页面文件或拆块
AWE映射1GB窗口耗时毫秒级到数十毫秒窗口切换是否可接受
文件映射缺页率随块大小变化块太小I/O多,块太大地址紧

我踩过的一个坑是:探针放在业务跑完之后,结果最大连续块只有300MB,误以为大地址标志没生效,回头查了半天PE头。后来把探针挪到main开头,同一台机器最大预留块到了1.8GB。地址空间碎片这个事很玄学,早占位比晚补救便宜。希望帮到你。

本文还有配套的精品资源,点击获取