摘要: 本文主要对Windows内存管理中的堆管理技术进行讨论,并简要介绍了堆的创建、内存块的分配与再分配、堆的撤销以及new和delete操作符的使用等内容。
关键词: 堆;堆管理
1 引言
在大多数Windows应用程序设计中,都几乎不可避免的要对内存进行操作和管理。在进行大尺寸内存的动态分配时尤其显的重要。本文即主要对内存管理中的堆管理技术进行论述。
堆(Heap)实际是位于保留的虚拟地址空间中的一个区域。刚开始时,保留区域中的多数页面并没有被提交物理存储器。随着从堆中越来越多的进行内存分配,堆管理器将逐渐把更多的物理存储器提交给堆。堆的物理存储器从系统页文件中分配,在释放时有专门的堆管理器负责对已占用物理存储器的回收。堆管理也是Windows提供的一种内存管理机制。主要用来分配小的数据块。与Windows的其他两种内存管理机制虚拟内存和内存映射文件相比,堆可以不必考虑诸如系统的分配粒度和页面边界之类比较烦琐而又容易忽视的问题,可将注意力集中于对程序功能代码的设计上。但是使用堆去分配、释放内存的速度要比其他两种机制慢的多,而且不具备直接控制物理存储器提交与回收的能力。
在进程刚启动时,系统便在刚创建的进程虚拟地址空间中创建了一个堆,该堆即为进程的默认堆,缺省大小为1MB,该值允许在链接程序时被更改。进程的默认堆是比较重要的,可供众多Windows函数使用。在使用时,系统必须保证在规定的时间内,每此只有一个线程能够分配和释放默认堆中的内存块。虽然这种限制将会对访问速度产生一定的影响,但却可以保证进程中的多个线程在同时调用各种Windows函数时对默认堆的顺序访问。在进程中允许使用多个堆,进程中包括默认堆在内的每个堆都有一个堆句柄来标识。与自己创建的堆不同,进程默认堆的创建、销毁均由系统来完成,而且其生命期早在进程开始执行之前就已经开始,虽然在程序中可以通过GetProcessHeap()函数得到进程的默认堆句柄,但却不允许调用HeapDestroy()函数显式将其撤消。
2 对动态创建堆的需求
前面曾提到,在进程中除了进程默认堆外,还可以在进程虚拟地址空间中动态创建一些独立的堆。至于在程序设计时究竟需不需要动态创建独立的堆可以从是否有保护组件的需要、是否能更加有效地对内存进行管理、是否有进行本地访问的需要、是否有减少线程同步开销的需要以及是否有迅速释放堆的需要等几个方面去考虑。
对于是否有保护组件的需要这一原则比较容易理解。在图1中,左边的图表示了一个链表(节点结构)组件和一个树(分支结构)组件共同使用一个堆的情况。在这种情况下,由于两组件数据在堆中的混合存放,如果节点3(属于链表组件)的后几个字节由于被错误改写,将有可能影响到位于其后的分支2(属于树组件)。这将致使树组件的相关代码在遍历其树时由于内存被破坏而无法进行。究其原因,树组件的内存是由于链表组建对其自身的错误操作而引起的。如果采用右图所示方式,将树组件和链表组件分别存放于一个独立的堆中,上述情况显然不会发生,错误将被局限于进行了错误操作的链表组件,而树组件由于存放在独立的堆中而受到了保护。
图1 动态创建堆在保护组件中的作用
在上图中,如果链表组件的每个节点占用12个字节,每个树组件的分支占用16个字节如果这些长度不一的对象共用一个堆(左图),在左图中这些已经分配了内存的对象已占满了堆,如果其中有节点2和节点4释放,将会产生24个字节的碎片,如果试图在24个字节的空闲区间内分配一个16字节的分支对象,尽管要分配的字节数小于空闲字节数,但分配仍将失败。只有在堆栈中分配大小相同的对象才可以实行更加有效的内存管理。如果将树组件换成其他长度为12字节的组件,那么在释放一个对象后,另一个对象就可以恰好填充到此刚释放的对象空间中。
进行本地访问的需要也是一条比较重要的原则。系统会经常在内存与系统页文件之间进行页面交换,但如果交换次数过多,系统的运行性能就将受很大的影响。因此在程序设计时应尽量避免系统频繁交换页面,如果将那些会被同时访问到的数据分配在相互靠近的位置上,将会减少系统在内存和页文件之间的页面交换频率。
线程同步开销指的是默认条件下以顺序方式运行的堆为保护数据在多个线程试图同时访问时不受破坏而必须执行额外代码所花费的开销。这种开销保证了堆对线程的安全性,因此是有必要的,但对于大量的堆分配操作,这种额外的开销将成为一个负担,并降低程序的运行性能。为避免这种额外的开销,可以在创建新堆时通知系统只有单个线程对访问。此时堆对线程的安全性将有应用程序来负责。
最后如果有迅速释放堆的需要,可将专用堆用于某些数据结构,并以整个堆去释放,而不再显式地释放在堆中分配的每一个内存块。对于大多数应用程序,这样的处理将能以更快的速度运行。