我正在参与「启航方案」
工作中遇到了 32位 windows 程序虚拟内存不足的问题,所以对 Windows 内存相关常识做了调研探索。文章内容总结自《Windows Internal》和 MSDN 文档,详细链接会注在文章最终,供咱们参阅
预备常识
在了解 Windows 内存常识前,需求澄清「虚拟内存」和「物理内存」的联系
虚拟内存和物理内存的联系
首要,了解一下内存分配进程涉及到的一些概念:
- 进程分配的都是虚拟内存,不能直接运用物理内存
- 虚拟内存地址经过 MMU (Mememory Management Unit),会被翻译为物理地址,找到对应的物理页
- 分配接连的虚拟内存,对应的物理内存纷歧定是接连的,好处是在进程层面不必过多考虑内存碎片化的影响
- 页射中,物理内存中存在对应的物理页
- 缺页(paging fault)反常,物理内存中没有找到对应的物理页
- 交流(swapping)或页面调度(paging),将当时没用的物理页(献身页)写入磁盘,将需求用的虚拟内存页映射到物理内存页
总的来说,咱们的程序用的都是虚拟内存,操作体系和硬件协助咱们将虚拟地址翻译为真实的物理地址,然后程序才干拜访到内存中的数据。
比方图中所示,物理内存一共只要4页。开始时,「进程A」分配了 4 页内存,此刻物理内存现已占满。此刻假如「进程B」又分配了 2 页内存「VP3」「VP4」,这时会触发缺页反常,操作体系会根据缓存策略将短时间用不到的内存数据交流到磁盘,比方「进程A」的 「VP3」「VP4」被换出到磁盘。然后,「进程B」的「VP3」「VP4」才干被运用。
上面的例子只是协助咱们大致了解内存分配的流程,实际情况会更加复杂,涉及到缓存优化,空间优化等进程,本文不再赘述。
咱们还能够观察到,图中的虚拟内存处在不同的状况,「Reserved」「Commited」,这两个状况代表了什么呢?请继续看下节。
Windows中虚拟内存的两种状况 reserved & comitted
- reserved 预留,表明预先分配的虚拟内存,但还没有映射到物理内存,在运用时需求先射中物理页
- commited 现已提交,表明虚拟内存现已映射到了物理内存或现已缓存在磁盘
- commited pages 也是 private pages,表明不能与其他进程同享
为什么虚拟内存需求 reserved,而不是直接运用 commited?
这是我在 stackoverflow 上找到的我比较认可的回答:
Why would I want toreserve? Why not just get committed memory? There are several reasons I have in mind:
- Some application needs a specific address range, say from 0x400000 to 0x600000, but does not need the memory for storing anything. It is used to trap memory access. E.g., if some code accesses such area, it will be caught. (Useful for some reason.)
- Some thread needs to store progressively expanding data. And the data needs to be in one contiguous chunk of memory. It is preferred not to commit large physical memory at one go because it is not needed and would be such a waste. The memory can be utilized by some other threads first. The physical memory is committed only on demand.
翻译一下:
- 某些运用需求特定的地址空间用于捕获内存捕获监测,一但某些代码拓荒了这块空间,就捕获这个事情
- 预留接连的空间,后续再运用,比方拓荒一条线程时,会先预留 1MB 的空间,而不会直接提交到物理内存
关于「32位程序」和「32位CPU」的 Q&A
Q1. 为什么 8G 乃至 16G 物理内存的笔记本电脑跑 winp32 程序仍是会 OOM?
A:win32程序的内存瓶颈在于虚拟内存不足,而不是物理内存
下面做个比喻,解说 32位程序虚拟内存和物理内存的联系是什么。
比方虚拟内存是校园,物理内存是宿舍。
-
校园盖的大,能招的学生就多,程序能分配的虚拟内存空间就大。
-
假如校园盖的小,宿舍盖的大,那么宿舍一定会有空位,因为校园就算招满人了,宿舍也住不满(代表了单进程,虚拟内存小于物理内存的情况,不考虑运用 PAE 技能的情况)
-
假如校园盖的大,宿舍盖的小,宿舍就会住满。那么就需求设定策略,让更需求住宿的同学住进宿舍,不太需求住宿的同学就要搬出宿舍,给需求的同学腾出位置(代表了虚拟内存大于物理内存的情况下,物理内存打满后,需求将不需求的内存数据写入磁盘)
Q2. 为什么32位程序瓶颈是在虚拟内存上?
A: 32位进程,虚拟内存空间是 4GB,Windows体系中,内核空间占用 2GB,用户空间只要 2GB
32位程序\操作体系的指针只能表明 2^32 = 4GB 规模内的地址,所以咱们拓荒的虚拟内存也只能在 4GB 以内。
一个进程的内存空间布局是什么姿态,为什么咱们可用的空间只要 2GB 会在介绍 Windows 进程内存布局一节中回答。
Q3. 32位CPU和32位操作体系的联系是什么?
A:32位操作体系的一条指令是32位,32位CPU一个时钟周期正好处理一条32位指令
-
32位CPU 是不能运用 64 位操作体系的,因为 64位操作体系一条指令是 64位,32位 CPU 无法处理
-
反过来,64位CPU 能够运转 32位操作体系,但无法发挥出 CPU 的悉数才干,有点「大马拉小车」的感觉
Q4. 32位CPU只能运用 4GB 的物理内存么?CPU的寻址才干和CPU的位宽相关么?
A:不是。不相关,CPU的寻址规模和CPU的位宽毫无联系
-
寻址规模和地址线宽度有关,和 CPU 位宽无关,Intel 32位CPU 早在1995年就支撑36位地址线了,也便是 32位CPU 能运用 64GB 的物理内存
-
为什么能拜访更大的内存地址?能够详细了解 PAE(Physical Address Extension) 技能
-
PAE 技能是为了让多个 32位进程累计运用内存的情况下,能运用更多的物理内存(超越4GB)
Windows 内存布局(Windows Process Virtual Space)
用户地址空间(User Address Space Layout)
咱们重点关注咱们能用到的地址空间是什么姿态的,对内核空间感兴趣的同学能够自己查阅其他材料。
下图出自《Windows Internals 6》
咱们知道程序需求先被加载到内存中,才干运转
上图描述了 x86(32位)进程的内存布局:
- 分为了 3GB 的用户空间,和 1GB 的内核空间,但这并不是 Win32 程序的正常布局,而是敞开了大地址空间形式的程序(LARGE_ADDRESS_AWARE)
- 正常的 Win32 程序用户空间只要 2GB,内核空间也占用 2GB
- 用户空间占用低地址(00000000 ~ 7FFFEFFF),内核空间占用高地址(7FFF000 ~ FFFFFFFF)
- 用户空间寄存了「代码」「全局变量」「线程栈」「DLL」等
- 内核空间图中详细标明晰包含什么,本文不再赘述,感兴趣的同学能够自行了解
上图详细描述了用户空间的布局:
- 最低地址寄存了 .exe
- 然后是 .dll
- 然后是 Heap,Heap 中寄存的是经过
HeapAlloc
等 API 分配的堆内存 - 然后是 Thread Stack,寄存的是线程栈内存,每开一条新线程就会对应拓荒一块栈内存
图中还提到了 ASLR,这是什么,后文会详细介绍。
下面,再来看一张图,此图出自《程序员的自我修养》
图中描述的用户空间非常「碎片化」,这或许也和 ASLR 相关。假如你要剖析运用的虚拟内存布局,不要完全以图中的布局为准,要以自己程序真实运转的情况为准。
这是书中对地址空间怎么计算的一些描述:
- 线程栈、进程堆、已装载的镜像文件(exe、dll)的地址是动态计算获得的
- 其中 exe dll 需求运用支撑 ASLR(随机选择地址)
ASLR 是什么?
下面详细看看,究竟什么是 ASLR
- ASLR 全称是 Address Space Layout Randomization,能够翻译为随机地址空间
- 目的是为了防护恶意软件做注入攻击,因为固定地址更简单被攻击者破译
- 这么做随之而来的缺点是更简单造成「内存碎片化」
怎么封闭 ASLR?
修正链接器高级配置,封闭随机基址(/DYNAMICBASE:NO)
此才干我没有亲自实验过,有需求的同学能够自己尝试
在 Windows 中,Memory Manager 会为每个线程供给两个栈,用户栈(user stack)和内核栈(kernel stack)
咱们仍然只总结用户栈
-
线程创立时,默许预留 1MB 虚拟内存
-
经过编译器指定参数 /STACK:reverse 能够将预留内存巨细写入 PE Header 中(修正 stack size)
-
虽然预留了 1 MB 虚拟内存,但只要 first page 虚拟内存会被提交(真实分配)
- 64 位体系跑 32 位程序,最大线程数量比 32 位机器跑 32 程序要少
- 原因是 64 位机器跑 32 位程序,会额外创立 64 位的栈,同样只要 2GB 虚拟内存空间,但每个线程重复消耗了两份内存
- 实测,64 位栈占用 256 kb 内存,每个线程栈算计占用 1.25 MB
总结,理论上在 64位体系上跑 32位程序,会有额外的开支,原本 32 位程序虚拟内存只要 2GB 可用,运转在 64 位体系上时会更快的露出这个短板。想了解更多的同学能够去查阅一下 WoW64(windows on windows64)
相关内容
剖析 Windows 虚拟内存的利器,VMMap
上面介绍了那么多理论,实际上咱们该怎么剖析运用的虚拟内存呢?
官方为咱们供给了一款东西 vmmap
内存区域意义
-
Total::总的分配过的虚拟内存
-
Free:可用的虚拟内存
-
Image:exe dll 占用的虚拟内存
-
Private data:进程私有的堆占用的内存
-
Stack:线程栈占用的虚拟内存
咱们也能够翻开 vmmap 点 help 进行查看每个区域的详细意义
CLI
除了 GUI,vmmap 也供给了 CLI 供咱们在脚本中运用
怎么处理 Win32 程序的虚拟内存瓶颈?
介绍了理论和东西,怎么处理实际问题呢?
将 32位程序升级为 64位
虚拟内存在 64位程序上将不会成为瓶颈,但将现有程序改为 64位并不是一件简单的事,详细需求做什么就不再本文赘述了。
缩小冗余的预留空间(Reserved)
- 减小线程栈分配空间,在上文得出结论,默许情况下,32位程序跑在64位体系上,每条线程需求拓荒 1.25MB内存,那咱们能够恰当减小栈巨细。假如是 java 程序能够经过JVM发动参数
Xss
来减少栈空间 - 减少大的预留的堆空间,比方 java 程序在 JVM 发动的时分就会预留分配 XmX 巨细的空间,假如是 1GB,就占用了一半的空间。
扩大进程虚拟内存空间
-
默许情况,进程虚拟内存巨细 2GB
-
假如
exe
做大地址空间标记且体系发动运用了特殊参数,能够将进程虚拟内存巨细升至 3GB
下面讲详细该怎么做
- 在编译 exe 的时分需求指定 Linker 参数
LARGE_ADDRESS_AWARE
为 YES - 需求用管理员形式翻开 cmd,然后输入命令
bcdedit /set increaseuserva 3072
,3072 表明 3GB
怎么查看大地址空间形式是否收效?
- 确认 windows 体系是否经过 bcdedit 设置了参数,用管理员形式翻开 cmd,输入
bcdedit
,看列表中是否有 increaseuserva 3072,假如有就进行下一步 - 运用 dumpbin /headers 查看 exe 是否敞开了大地址空间形式
参阅
《Windows Internal 6》《Windows Internal 7》 《程序员的自我修养》
hansimov.gitbook.io/csapp/part2…
docs.microsoft.com/en-us/windo…
news.mydrivers.com/1/571/57139…
www.zhihu.com/question/38…
stackoverflow.com/questions/1…
stackoverflow.com/questions/9…
docs.microsoft.com/en-us/cpp/b…
stackoverflow.com/questions/2…
docs.microsoft.com/en-us/archi…