1 引言
在大多数Windows应用程序设计中,都几乎不可避免的要对内存进行操作和管理。在进行大尺寸内存的动态分配时尤其显的重要。本文即主要对内存管理中的堆管理技术进行论述。
堆(Heap)实际是位于保留的虚拟地址空间中的一个区域。刚开始时,保留区域中的多数页面并没有被提交物理存储器。随着从堆中越来越多的进行内存分配,堆管理器将逐渐把更多的物理存储器提交给堆。堆的物理存储器从系统页文件中分配,在释放时有专门的堆管理器负责对已占用物理存储器的回收。堆管理也是Windows提供的一种内存管理机制。主要用来分配小的数据块。与Windows的其他两种内存管理机制(1)虚拟内存和(2)内存映射文件相比,堆可以不必考虑诸如系统的分配粒度和页面边界之类比较烦琐而又容易忽视的问题,可将注意力集中于对程序功能代码的设计上。但是使用堆去分配、释放内存的速度要比其他两种机制慢的多,而且不具备直接控制物理存储器提交与回收的能力。
在进程刚启动时,系统便在刚创建的进程虚拟地址空间中创建了一个堆,该堆即为进程的默认堆,缺省大小为1MB,该值允许在链接程序时被更改(即,在VS编译程序的时候,在项目设置-连接器设置-中可以设置)。进程的默认堆是比较重要的,可供众多Windows函数使用。在使用时,系统必须保证在规定的时间内,每此只有一个线程能够分配和释放默认堆中的内存块。虽然这种限制将会对访问速度产生一定的影响,但却可以保证进程中的多个线程在同时调用各种Windows函数时对默认堆的顺序访问。
在进程中允许使用多个堆,进程中包括默认堆在内的每个堆都有一个堆句柄来标识。与自己创建的堆不同,进程默认堆的创建、销毁均由系统来完成,而且其生命期早在进程开始执行之前就已经开始,虽然在程序中可以通过GetProcessHeap()函数得到进程的默认堆句柄,但却不允许调用HeapDestroy()函数显式将其撤消。
2. 对动态创建堆的需求
前面曾提到,在进程中除了进程默认堆外,还可以在进程虚拟地址空间中动态创建一些独立的堆。至于在程序设计时究竟需不需要动态创建独立的堆可以从是否有保护组件的需要、是否能更加有效地对内存进行管理、是否有进行本地访问的需要、是否有减少线程同步开销的需要以及是否有迅速释放堆的需要等几个方面去考虑。
(1)对于是否有保护组件的需要这一原则比较容易理解。在图1中,左边的图表示了一个链表(节点结构)组件和一个树(分支结构)组件共同使用一个堆的情况。在这种情况下,由于两组件数据在堆中的混合存放,如果节点3(属于链表组件)的后几个字节由于被错误改写,将有可能影响到位于其后的分支2(属于树组件)。这将致使树组件的相关代码在遍历其树时由于内存被破坏而无法进行。究其原因,树组件的内存是由于链表组建对其自身的错误操作而引起的。如果采用右图所示方式,将树组件和链表组件分别存放于一个独立的堆中,上述情况显然不会发生,错误将被局限于进行了错误操作的链表组件,而树组件由于存放在独立的堆中而受到了保护。
【图1】
在上图中,如果链表组件的每个节点占用12个字节,每个树组件的分支占用16个字节如果这些长度不一的对象共用一个堆(左图),在左图中这些已经分配了内存的对象已占满了堆,如果其中有节点2和节点4释放,将会产生24个字节的碎片,如果试图在24个字节的空闲区间内分配一个16字节的分支对象,尽管要分配的字节数小于空闲字节数,但分配仍将失败。只有在堆栈中分配大小相同的对象才可以实行更加有效的内存管理。如果将树组件换成其他长度为12字节的组件,那么在释放一个对象后,另一个对象就可以恰好填充到此刚释放的对象空间中。
(2)进行本地访问的需要也是一条比较重要的原则。系统会经常在内存与系统页文件之间进行页面交换,但如果交换次数过多,系统的运行性能就将受很大的影响。因此在程序设计时应尽量避免系统频繁交换页面,如果将那些会被同时访问到的数据分配在相互靠近的位置上,将会减少系统在内存和页文件之间的页面交换频率。
(3)线程同步开销指的是默认条件下以顺序方式运行的堆为保护数据在多个线程试图同时访问时不受破坏而必须执行额外代码所花费的开销。这种开销保证了堆对线程的安全性,因此是有必要的,但对于大量的堆分配操作,这种额外的开销将成为一个负担,并降低程序的运行性能。为避免这种额外的开销,可以在创建新堆时通知系统只有单个线程对访问。此时堆对线程的安全性将有应用程序来负责。
(4)最后如果有迅速释放堆的需要,可将专用堆用于某些数据结构,并以整个堆去释放,而不再显式地释放在堆中分配的每一个内存块。对于大多数应用程序,这样的处理将能以更快的速度运行。
3. 创建堆
在进程中,如果需要可以在原有默认堆的基础上动态创建一个堆,可由HeapCreate()函数完成:
HANDLE HeapCreate( DWORD flOptions, DWORD dwInitialSize, DWORD dwMaximumSize );
其第一个参数flOptions指定了对新建堆的操作属性。该标志将会影响一些堆函数如HeapAlloc()、HeapFree()、HeapReAlloc()和HeapSize()等对新建堆的访问。其可能的取值为下列标志及其组合:
参数dwInitialSize和dwMaximumSize分别为堆的初始大小和堆栈的最大尺寸。其中,dwInitialSize的值决定了最初提交给堆的字节数。如果设置的数值不是页面大小的整数倍,则将被圆整(Round Up)到邻近的页边界处。而dwMaximumSize则实际上是系统能为堆保留的地址空间区域的最大字节数。如果该值为0,那么将创建一个可扩展的堆,堆的大小仅受可用内存的限制。如果应用程序需要分配大的内存块,通常要将该参数设置为0。如果dwMaximumSize大于0,则该值限定了堆所能创建的最大值,HeapCreate()同样也要将该值圆整到邻近的页边界,然后再在进程的虚拟地址空间为堆保留该大小的一块区域。在这种堆中分配的内存块大小不能超过0x7FFF8字节,任何试图分配更大内存块的行为将会失败,即使是设置的堆大小足以容纳该内存块。
如果HeapCreate()成功执行,将会返回一个标识新堆的句柄,并可供其他堆函数使用。
需要特别说明的是,在设置第一个参数时,对HEAP_NO_SERIALIZE的标志的使用要谨慎,一般应避免使用该标志。这是同后续将要进行的堆函数HeapAlloc()的执行过程有关系的,在HeapAlloc()试图从堆中分配一个内存块时,将执行下述几步操作:
1) 遍历分配的和释放的内存块的链接表
2) 搜寻一个空闲内存块的地址
3) 通过将空闲内存块标记为"已分配"来分配新内存块
4) 将新分配的内存块添加到内存块列表
如果这时有两个线程1、2试图同时从一个堆中分配内存块,那么线程1在执行了上面的1和2步后将得到空间内存块的地址。但是由于CPU对线程运行时间的分片,使得线程1在执行第3步操作前有可能被线程2抢走执行权并有机会去执行同样的1、2步操作,而且由于先执行的线程1并没有执行到第3步,因此线程2会搜寻到同一个空闲内存块的地址,并将其标记为已分配。而线程1在恢复运行后并不能知晓该内存块已被线程2标记过,因此会出现两个线程军认为其分配的是空闲的内存块,并更新各自的联接表。显然,象这种两个线程拥有完全相同内存块地址的错误是非常严重而又是难以发现的。
由于只有在多个线程同时进行操作时才有可能出现上述问题,一种简单的解决的办法就是不使用HEAP_NO_SERIALIZE标志而只允许单个线程独占地对堆及其联接表拥有访问权。如果一定要使用此标志,为了安全起见,必须确保进程为单线程的或是在进程中使用了多线程,但只有单个线程对堆进行访问。再就是使用了多线程,也有多个线程对堆进行了访问,但这些线程通过使用某种线程同步手段。如果可以确保以上几条中的一条成立,也是可以安全使用HEAP_NO_SERIALIZE标志的,而且还将拥有快的访问速度。如果不能肯定上述条件是否满足,建议不使用此标志而以顺序的方式访问堆,虽然线程速度会因此而下降但却可以确保堆及其中数据的不被破坏。
4. 从堆中分配内存块
在成功创建一个堆后,可以调用HeapAlloc()函数从堆中分配内存块。
在此,该函数可以从两个种堆中分配内存块。(1)从用HeapCreate()创建的动态堆中分配内存块,(2)也可以直接从进程的默认堆中分配内存块。
下面先给出HeapCreate()的函数原型
LPVOID HeapAlloc( HANDLE hHeap, DWORD dwFlags, DWORD dwBytes );
其中,参数hHeap为要分配的内存块来自的堆的句柄(从分配的哪个堆中进行分内存块),可以是从HeapCreate()创建的动态堆句柄也可以是由GetProcessHeap()得到的默认堆句柄。
参数dwFlags指定了影响堆分配的各个标志。该标志将覆盖在调用HeapCreate()时所指定的相应标志,可能的取值为:
最后一个参数dwBytes设定了要从堆中分配的内存块的大小。如果HeapAlloc()执行成功,将会返回从堆中分配的内存块的地址。如果由于内存不足或是其他一些原因而引起HeapAlloc()函数的执行失败,将会引发异常。通过异常标志可以得到引起内存分配失败的原因:如果为STATUS_NO_MEMORY则表明是由于内存不足引起的;如果是STATUS_Access_VIOLATION则表示是由于堆被破坏或函数参数不正确而引起分配内存块的尝试失败。以上异常只有在指定了HEAP_GENERATE_EXCEPTIONS标志时才会发生,如果没有指定此标志,在出现类似错误时HeapAlloc()函数只是简单的返回NULL指针。
在设置dwFlags参数时,如果先前用HeapCreate()创建堆时曾指定过HEAP_GENERATE_EXCEPTIONS标志,就不必再去设置HEAP_GENERATE_EXCEPTIONS标志了,因为HEAP_GENERATE_EXCEPTIONS标志已经通知堆在不能分配内存块时将会引发异常。另外,对HEAP_NO_SERIALIZE标志的设置应慎重,与在HeapCreate()函数中使用HEAP_NO_SERIALIZE标志类似,如果在同一时间有其他线程使用同一个堆,那么该堆将会被破坏。如果是在进程默认堆中进行内存块的分配则要绝对禁用此标志。
在使用堆函数HeapAlloc()时要注意:堆在内存管理中的使用主要是用来分配一些较小的数据块,如果要分配的内存块在1MB左右,那么就不要再使用堆来管理内存了,而应选择虚拟内存的内存管理机制。
5. 再分配内存块
在程序设计时经常会由于开始时预见不足而造成在堆中分配的内存块大小的不合适(多数情况是开始时分配的内存较小,而后来实际需要更多的数据复制到内存块中去)这就需要在分配了内存块后再根据需要调整其大小。堆函数HeapReAlloc()将完成这一功能,其函数原型为:
参考:
https://www.cnblogs.com/findumars/p/5929832.html
原文地址:https://www.cnblogs.com/icmzn/p/11823744.html