让我们来了解一下Windows中内存管理的一些知识:
1. 机器的物理内存由两部分组成。一部分为机器的主存RAM,也就是我们内存条的大小;另一部分为虚拟内存,它就在机器的硬盘上,以页文件的形式存在。
2. 每个进程都有自己的虚拟地址空间,对于具有32位寻址能力的机器来说,这个虚拟空间的大小为4GB。现在我们使用的机器就是4GB。
3. 进程的4GB虚拟地址空间又可以分成几个部分,其中进程真正私有的空间少于2GB(这段地址空间被称作“用户方式分区”),其余的2GB多空间都是给操作系统的,且这部分空间被所有的进程共享。(参考Windows核心编程Chapter 13)
4. 为进程“分配内存”,这个概念可以细化:“保留一段地址空间”,“提交一段内存空间”,“将内存空间映射到主存”。在程序中我们通常所访问的地址都必须是进程地址空间中被保留和提交的那段地址空间。
4.1 “保留一段地址空间”:即从进程的4GB地址空间中保留一段地址空间,这个过程通过VirtualAlloc函数完成,并把分配类型参数设置为MEM_RESERVE。这段空间的起始地址必须是系统分配粒度的整数倍,大小必须是系统页面大小的整数倍。
4.2 “提交一段内存空间”:即为进程已保留的地址空间映射机器的物理内存,这里要特别注意,所谓物理内存一般并不是机器的主存,而只是机器的虚拟内存。这个过程同样又VirtualAlloc完成,只是把分配类型参数设置为MEM_COMMIT。这段空间的起始地址和大小都必须是页面大小的整数倍。这样进程的对应被提交的区域就被映射到机器的虚拟内存上。
4.3 “将内存空间映射到主存”:这点很重要,操作系统总是只有在进程提交的页面被访问时才将相应的页面加载到主存中,同时修改进程对应页面的地址空间映射。这时,进程的地址空间中的对应区域才和机器上的主存对应起来。
Virtual Size:
该指标记录了当前进程申请成功的其虚拟地址空间的总的空间大小,包括DLL/EXE占用的地址和通过VirtualAlloc API Reserve的Memory Space数量。请注意,该指标包括保留的地址空间。
Private Bytes:
该指标记录了进程用户方式分区地址空间中已提交的总的空间大小。无论是直接调用API申请的内存,被Heap Manager申请的内存,或者是CLR 的managed heap,都算在里面。
Working Set:
该指标记录了所有映射到进程虚拟地址空间的机器主存的大小,它不仅仅是用户方式分区部分的映射,而是整个进程地址空间的映射。即它同时包括内核方式分区中映射到机器主存的部分。由4.3可知,在用户方式分区部分只有在进程提交的页面被访问时才将相应的页面加载到主存中。而对于该部分的大小总是系统页面大小的整数倍。
这里有一个问题,随着进程的不断运行,进程被访问的页面将可能不断增加,这是否意味着“Working Set”的大小会不断的累加呢?显然不是。在程序运行过程中影响“Working Set”的因素包括:(1) 机器可用主存的大小 (2) 进程本身“Working Set”的大小范围。当机器的可用主存小于一定值时,系统会释放一些老的最近没有被访问的页面,把这些页面通过交换文件交换到机器的虚拟内存中;当Working Set的大小大于该进程所设置的最大值时,同样会把一些老的页面交换到机器的虚拟内存中。当这些页面下次再被访问时,它们才加载到主存。
由上可知,”Working Set“一定比”Private Bytes“小,因为它只是”Private Bytes“对应的地址空间中被加载到主存的那部分。
“Page Faults”
该指标和”Working Set“密切相关,当进程访问某个页面,而这个页面却不在主存中时,就要发生一次“Page Fault“,即进程访问非”Working Set“中的页面时,发生一次”Page Fault“,同时系统将对应页面加载到主存中。
接下来的三个指标是对”Working Set“的细化:
”WS Private“
该指标记录了进程”Working Set“中被该进程所独享的空间大小。
"WS Shareable"
该指标记录了进程”Working Set“中能与别的进程共享的空间大小
”WS Shared“
该指标记录了进程”Working Set“中已经与别的进程共享的空间大小
”WS Shareable“和”WS Shared“两个指标乍一看令人感到疑惑,因为既然”Working Set“属于”Private Bytes“中的一部分,而”Private Bytes“是进程私有的,为什么会有”WS Shareable“和”WS Shared“这两项呢?
认真一想,其实很容易理解,比如两个进程都需要同一个DLL的支持,所以在进程运行过程中,这个DLL被映射到了两个进程的地址空间中,如果这个DLL的大小为4K,在两个进程中都要提交4K的虚拟地址空间来映射这个DLL。当第一个进程访问了这个DLL时,这个DLL被加载到机器主存中,这时,第二个进程也要访问该DLL,这时,系统就不会再加载一遍该DLL了,因为这个DLL已经在主存中了。当然上面所说的访问仅仅是读取的操作,如果这时候某个进程要修改DLL对应这段地址中的某个单元时,这时,系统必须为第二个进程分配另外的新页面,并把要修改位置对应的页面拷贝的这个新页面,同时,第二个进程中的这个DLL被映射到这个新页面上。
上面的分析中,DLL对应的4K的内存在第一个进程中便是”WS Shareable“。另外,内核方式分区中的所有代码都是被所有进程共享的,只要一个进程访问了这些页面,则在所有的进程的”Working Set“中都能体现。
三、下面我们来讨论一下这些内存指标与进程内存消耗之间的关系
在计算机更新换代不断加速的今天,我们往往很少关注程序对内存的消耗,除非程序的内存消耗超出了我们的忍受范围——大量的泄漏、运行速度下降等。
那么,当我们在测进程的内存使用量时,到底应该使用哪个指标能更好的反应程序的内存消耗呢?由于Windows自带的Task Manager中的”Memory Usage“所对应的指标就是”Working Set“,所以大部分人认为该指标能够很好的反应进程的内存使用量。
在得出结论之前,让我们来分析一下以上的这些指标:
就从”Working Set“开始吧。
”Working Set“:
进程中被加载到机器主存的所有页面大小的和。它可细分为”WS Shareable“和”WS Shared“。进程访问页面不再”Working Set“中时,会发生一次”Page Fault“且同时发生一次主存与虚拟内存之间的数据交换。综上所述,我们可以得出结论:
(a)”Working Set“不是进程内存消耗的全部;
(b)所有进程”Working Set“的和也不等有机器主存总的消耗量,因为存在”Working Shareable“与别的进程共享;
(c)”Working Set“太大会影响机器的运行速度,因为”Working Set“太大会导致机器的可用主存太少,从而导致将进程的老页面释放到虚拟内存,同时,进程”Working Set“中的页面减少后,使进程发生”Page Fault“的频率更高。因为在主存与虚拟内存之间交换数据需要时间,所以机器的运行速度要减慢。
(d)”Working Set“由于数据交换的存在,该指标是动态的,在测量的过程中会不断变化。(变化的最小单位为4K)
所以”Working Set“指标强调的是进程对机器主存的消耗,不是进程内存的全部信息。
"Private Bytes"
该指标包含所有为进程提交的内存,包括机器主存和虚拟内存,可以认为它是进程对物理内存消耗,且该指标相对来说更加稳定。在程序产生内存泄漏时,该值一定是不断上涨的。
综上所述,个人更倾向于使用”Private Bytes“来定量进程的内存消耗和分析进程的内存泄漏。
虚拟存储器是一个抽象概念,它为每一个进程提供了一个假象,好像每个进程都在独占的使用主存。每个进程看到的存储器都是一致的,称之为虚拟地址空间。
每个进程看到得虚拟地址空间有大量准确定义的区(area)构成,每个区都有专门的功能。从最低的地址看起:
· 程序代码和数据:代码是从同一固定地址开始,紧接着的是和C全局变量相对应的数据区。 (应该就是所谓的静态存储空间)
· 堆:代码和数据区后紧随着的是运行时堆。作为调用malloc和free这样的C标准库函数,堆可以在运行时动态的扩展和收缩。(应该就是所谓的动态存储区)
· 共享库:在地址空间的中间附近是一块用来存放像C标准库和数学库这样共享库的代码和数据的区域。(C标准库函数的指令,连接阶段把他们加入到编译后的程序)
· 栈:位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。和堆一样每次我们从函数返回时,栈就会收缩。
· 内核虚拟存储器:内核是操作系统总是驻留在存储器中的部分。地址空间顶部的四分之一部分是为内核预留的。(系统函数?这里说的UNIX系统,不知道windows下是不是这样的?)
今天大多数计算机的字长都是32字节,这就限制了虚拟地址空间为4千兆字节(4GB)
引言
Windows的内存结构是深入理解Windows操作系统如何运作的最关键之所在,通过对内存结构的认识可清楚地了解诸如进程间数据的共享、对内存进行有效的管理等问题,从而能够在程序设计时使程序以更加有效的方式运行。Windows操作系统对内存的管理可采取多种不同的方式,其中虚拟内存的管理方式可用来管理大型的对象和结构数组。
在Windows系统中,任何一个进程都被赋予其自己的虚拟地址空间,该虚拟地址空间覆盖了一个相当大的范围,对于32位进程,其地址空间为232=4,294,967,296 Byte,这使得一个指针可以使用从0x00000000到0xFFFFFFFF的4GB范围之内的任何一个值。虽然每一个32位进程可使用4GB的地址空间,但并不意味着每一个进程实际拥有4GB的物理地址空间,该地址空间仅仅是一个虚拟地址空间,此虚拟地址空间只是内存地址的一个范围。进程实际可以得到的物理内存要远小于其虚拟地址空间。进程的虚拟地址空间是为每个进程所私有的,在进程内运行的线程对内存空间的访问都被限制在调用进程之内,而不能访问属于其他进程的内存空间。这样,在不同的进程中可以使用相同地址的指针来指向属于各自调用进程的内容而不会由此引起混乱。下面分别对虚拟内存的各具体技术进行介绍。
地址空间中区域的保留与释放
在进程创建之初并被赋予地址空间时,其虚拟地址空间尚未分配,处于空闲状态。这时地址空间内的内存是不能使用的,必须首先通过VirtualAlloc()函数来分配其内的各个区域,对其进行保留。
LPVOID VirtualAlloc(
LPVOID lpAddress,
DWORD dwSize,
DWORD flAllocationType,
DWORD flProtect
);
其参数lpAddress包含一个内存地址,用于定义待分配区域的首地址。通常可将此参数设置为NULL,由系统通过搜索地址空间来决定满足条件的未保留地址空间。这时系统可从地址空间的任意位置处开始保留一个区域,而且还可以通过向参数flAllocationType设置MEM_TOP_DOWN标志来指明在尽可能高的地址上分配内存。如果不希望由系统自动完成对内存区域的分配而为lpAddress设定了内存地址(必须确保其始终位于进程的用户模式分区中,否则将会导致分配的失败),那么系统将在进行分配之前首先检查在该内存地址上是否存在足够大的未保留空间,如果存在一个足够大的空闲区域,那么系统将会保留此区域并返回此保留区域的虚拟地址,否则将导致分配的失败而返回NULL。这里需要特别指出的是,在指定lpAddress的内存地址时,必须确保是从一个分配粒度的边界处开始。
一般来说,在不同的CPU平台下分配粒度各不相同,但目前所有Windows环境下的CPU如x86、32位Alpha、64位Alpha以及IA-64等均是采用64KB的分配粒度。如果保留区域的起始地址没有遵循从64KB分配粒度的边界开始之一原则,系统将自动调整该地址到最接近的64K的倍数。例如,如果指定的lpAddress为0x00781022,那么此保留区域实际是从0x00780000开始分配的。参数dwSize指定了保留区域的大小。但是系统实际保留的区域大小必须是CPU页面大小的整数倍,如果指定的dwSize并非CPU页面的整数倍,系统将自动对其进行调整,使其达到与之最接近的页面大小整数倍。与分配粒度一样,对于不同的CPU平台其页面大小也是不一样的。在x86平台下,页面大小为4KB,在32位Alpah平台下,页面大小为8KB。在使用时可以通过GetSystemInfo()来决定当前主机的页面大小。参数flAllocationType和flProtect分别定义了分配类型和访问保护属性。由于VirtualAlloc()可用来保留一个区域也可以用来占用物理存储器,因此通过flAllocationType来指定当前是要保留一个区域还是要占用物理存储器。其可能使用的内存分配类型有:
分配类型 |
类型说明 |
MEM_COMMIT |
为特定的页面区域分配内存中或磁盘的页面文件中的物理存储 |
MEM_PHYSICAL |
分配物理内存(仅用于地址窗口扩展内存) |
MEM_RESERVE |
保留进程的虚拟地址空间,而不分配任何物理存储。保留页面可通过继续调用VirtualAlloc()而被占用 |
MEM_RESET |
指明在内存中由参数lpAddress和dwSize指定的数据无效 |
MEM_TOP_DOWN |
在尽可能高的地址上分配内存(Windows 98忽略此标志) |
MEM_WRITE_WATCH |
必须与MEM_RESERVE一起指定,使系统跟踪那些被写入分配区域的页面(仅针对Windows 98) |
分配成功完成后,即在进程的虚拟地址空间中保留了一个区域,可以对此区域中的内存进行保护权限许可范围内的访问。当不再需要访问此地址空间区域时,应释放此区域。由VirtualFree()负责完成。其函数原型为:
BOOL VirtualFree( |
其中,参数lpAddress为指向待释放页面区域的指针。如果参数dwFreeType指定了MEM_RELEASE,则lpAddress必须为页面区域被保留时由VirtualAlloc()所返回的基地址。参数dwSize指定了要释放的地址空间区域的大小,如果参数dwFreeType指定了MEM_RELEASE标志,则将dwSize设置为0,由系统计算在特定内存地址上的待释放区域的大小。参数dwFreeType为所执行的释放操作的类型,其可能的取值为MEM_RELEASE和MEM_DECOMMIT,其中MEM_RELEASE标志指明要释放指定的保留页面区域,MEM_DECOMMIT标志则对指定的占用页面区域进行占用的解除。如果VirtualFree()成功执行完成,将回收全部范围的已分配页面,此后如再对这些已释放页面区域内存的访问将引发内存访问异常。释放后的页面区域可供系统继续分配使用。
下面这段代码演示了由系统在进程的用户模式分区内保留一个64KB大小的区域,并将其释放的过程:
// 在地址空间中保留一个区域
LPBYTE bBuffer = (LPBYTE)VirtualAlloc(NULL, 65536, MEM_RESERVE, PAGE_READWRITE);
……
// 释放已保留的区域
VirtualFree(bBuffer, 0, MEM_RELEASE);