返回> 网站首页
内存整理的迷思
yoours2011-03-03 14:50:28
简介一边听听音乐,一边写写文章。
作者:Felix
看了接二连三出现于本组的有关内存整理的帖子,终于觉得有必要写一点文字了,这些帖子如果是在别的组尚且情有可原,可是出现在编程组中却实属不该,看来不少人仍然对Windows的内存管理机制存在种种误解,希望这篇短文能够澄清这些误解中的一部分(如果不是全部的话)。
* 进程内存布局
Win32中每个进程拥有4GB的虚拟内存地址空间。
典型的Winnt系统中的一个进程的内存布局如下。
+--------------+ 0xffffffff
| 系统代码 |
| 设备驱动 |
| 内存映射文件 |
+--------------+ 0x80000000
| 用户dll映像 |
+--------------+
| heap |
+--------------+
| stack |
+--------------+
| global |
+--------------+
| 用户exe映像 |
+--------------+ 0x00010000
| 保留 |
+--------------+
整个4GB虚拟地址空间分为两部分,上面2GB是系统代码,下面2GB是用户代码(用户区最底部的64KB空间为系统保留),
* 物理内存分页
以上是虚拟内存,再看物理内存。Windows通过2级页表来将虚拟内存地址映射到物理内存。如图所示:
+-----+ +------------------------------------------+
| CR3 | | 一级页表索引 | 二级页表索引 | 页内偏移量 | 32位虚拟地址格式
+-----+ +------------------------------------------+
| | | | | | | | | | 第一级页表 | 第二级页表 | 物理内存 +------+-> +-----------+ +--+-> +-----------+ +-+---> +-----------+
| | 页表入口 | | | | 页表入口 | | | | 4KB内存页 |
| +-----------+ | | +-----------+ | +---> | |
+-> | 页表入口 +--+ +-> | 页表入口 +--+ | |
+-----------+ +-----------+ +-----------+
| 共1024条 | | 共1024条 | | 4KB内存页 |
+-----------+ +-----------+ | |
| ... | | ... | | |
| | | | +-----------+
| | | | | ... |
| | | | | |
+-----------+ +-----------+ +-----------+
+------------+----------+
| 20位索引值 | 12位标志 | 页表入口格式
+------------+----------+
物理内存按4KB为单位划分为页面,给定一个32位虚拟地址,Windows首先从CR3寄存器取得第一级页表,然后从虚拟地址的一级页表索引字段取得一级页表入口,从一级页表入口的20位索引可找到对应的二级页表,然后从虚拟地址的二级页表索引字段取得二级页表入口,从二级页表入口的20位索引找到具体的4KB物理内存页,最后根据虚拟地址的页内偏移字段访问物理内存。听上去比较复杂,不过对照图片一看就很清楚了。每个进程都有自己的一套页表,对不同的进程,Windows只要在CR3寄存器装入不同的一级页表地址就可以了。(这里给出的是一个概念模型,实际上Windows对页表访问还有一些优化技巧)
为什么要分页呢,这是因为虚拟内存中的数据不一定必须在物理内存中,如果一个页面的数据在磁盘上,Windows就在对应的页表入口的标志位中做一个标记,这样访问到这个页面时就引发一个页面错误。Windows一旦捕捉到页面错误,就将相应的页面从磁盘载入物理内存并再次尝试读取,这个过程对应用程序来说是透明的,应用程序无需关心自己要访问的数据是在物理内存里还是在磁盘上。
* 内存分配
Windows应用程序使用VirtualAlloc API函数分配内存块。也许你用的编程语言使用不同的关键字,但最终它们都被转换为对VirtualAlloc的调用。VirtualAlloc分为两个步骤,第一步是保留,第二步是提交。保留的意思是将虚拟地址做个标记表示我预订了这个位置,接下来的分配就不会分配在已经被预定的位置了。提交的意思是实际准备开始用这个内存块。
* 懒惰策略
即使提交了内存块,Windows也并不立即为这段地址初始化页表。因为可能一段内存虽然被提交,某些区域却从来不使用,为这些地址构造页表完全是白费力气。Windows采取懒惰策略,一直到某个页面错误出现,才为那个页面创建页表。这个技术使得即使分配很大块的内存也可以在瞬间完成。
* 进程工作集
你可能在想,如果一个进程提交了1GB的虚拟内存,并且将这1GB虚拟内存全部访问一遍,那么是不是它就能占用整个计算机的所有物理内存呢?答案是否。
Windows启动时,根据计算机上安装的内存数量计算两个值“进程默认工作集大小”和“进程最大工作集大小”。每个进程以默认工作集大小启动。随着进程使用内存的增加,工作集可以渐渐增大,直到最大值。如果系统有足够的空闲页面,进程工作集甚至可以超过最大值,反正多出来的内存闲着也是闲着。如果系统没有多余的空闲页面,而进程又达到了最大工作集限制,对后续的页面错误,Windows先删除该进程的一个页面,然后将要求的页面载入。当空闲内存进一步减少时,Windows将开始缩小各个进程的工作集,将一些页面换出内存。
所以,一个恶意的或错误的程序实际上并没有办法用拼命分配内存的方法对系统造成过大的影响。
* 内存整理
有了上面这些知识,你就很容易看出来所谓的内存整理有多么荒谬。物理内存按4KB分页,根本无所谓碎片化,就算物理内存堆放得再整齐连续,系统总是按照4KB为单位访问它。
我所见的大多数内存整理程序的做法是分配一块很大的内存,意图将其他进程的数据换入磁盘,然后释放这块内存来得到大块物理内存。然而由于Windows的工作集裁剪策略,这个做法实际上无法起作用,如果系统的内存压力相当重,那么不管这个程序试图分配多少内存,结果只是导致自己的内存被换出,而不是其他进程的。
退一步说,即使这个动作能够起到将其他进程的内存换出的作用,但这实际上只是一个损害系统性能的动作,而不是一种优化,因为很快其他进程就会产生大量页面错误,结果就是硬盘猛转。
* 堆碎片化问题
整理物理内存虽然是无稽之谈,但进程的动态存储区--heap,确实是会有碎片化问题的。准确地说,这不是内存碎片,而是地址碎片。如果程序反复分配释放小块内存,heap的地址可能变得很不连续,虽然耗尽2GB虚拟地址的可能不大,但在碎片化的堆中寻找一块可用内存就会变得比较慢从而影响执行效率。
解决这个问题的方法只能是写程序的时候注意考虑这个问题,而不可能借助外部程序。例如使用一个内存池来管理自己的内存,Jeffrey Richter的 <Advanced Windows> 一书中介绍了一种重载class的operator new的方法。
文章评论
1300人参与,0条评论