Windows核心编程笔记(十七) 堆

1.

                 虚拟内存主要用于 分配连续的内存如给大数组分配内存
                 堆主要用于 分配小块内存如链表,树等就适合用堆来分配内存。
                优点在于:在堆中分配内存时不像虚拟内存要求那么多,如分配粒度、页边界等。它没有这些要求, 可分配任意大小的内存
                缺点在于:相对于其它内存分配机制,它 分配和释放内存的速度较慢,而且失去了对内存commit和decommit的控制。
                  一个进程中可以有多个堆。除默认堆外,其它堆都可以在进程生存期间创建或销毁
                进程的 默认堆用户不能销毁进程默认堆,因为在进程开始执行前,默认堆已经建立, 进程结束后它会被自动销毁
                 堆中的内存 分配操作是串行的,当有两个线程同时要在同一个堆中分配内存时,系统会 先让一个线程执行分配内存操作,待其毕,再让第二个线程做分配内存操作

 

2.创建其它堆的原因

                 模块保护:把 不同的类型数据放在不同的堆中,可以防止对其中一种数据进行写操作发生内存溢出时覆盖其它类数据的内容。
                              如:有一个链表和一棵树,若将它们的内存都放在同一个堆中,那么当对链表的内存操作有写溢出时可能会覆盖树中的内容

                                    把链表和树分别放在不同的堆中就可以保证链表的写溢出操作不会影响树中的数据。

                 有效的内存管理:在堆中仅 分配相同大小的对象有利用提高内存使用率,避免产生内存碎片
                              如:有10字节大小的堆,其中有两个2字节的对象,两个3字节的对象。此时内存已满。当释放两个不连续2字节的对象时
                              此时有4字节的空间可用,但由于不连续,就产生了内存碎片。 当需要3字节的连续空间时就无法提供足够的内存。 

                本地存储: 把可能被同时访问的数据连续放在一起(同一页中),减少换页操作和页的访问次数,有利于提高效率

                 避免线程同步开销:当多线程同时对同一堆进行操作时,堆函数需要执行额外的代码来同步线程的访问,这些额外执行的代码会对程序的性能造成影响。
                                         当我们 为新建堆时可以告诉系统,该堆只会被一个线程访问,这样堆函数就不会执行额外的代码,从而提高了系统的性能。

                                         此时由用户来控制堆的线程安全,可以用互拆量等对线程操作堆的操作进行同步 

 
                 快速释放内存:为某些数据结构指定专用堆,允许我们 对整个堆进行释放操作,而无需一块块地释放堆中的内存。
         
3.如何创建额外的堆   

               若要分配大量内存,如1M或更多,建议用VirtualAlloc来分配内存而不是在堆中分配内存 

                创建堆函数 HANDLE HeatCreate(DWORD, fwdOptions
                                               SIZE_T dwInitialSize,
                                               SIZE_T dwMaximumSize) 
               在创建堆时尽量 不要用fwdOptions,设为 HEAP_NO_SERIALIZE除非以下三种情况:
                                (1)进程中 只有一个线程
                                (2)多线程中确保 只有一个线程会对该堆进行操
                                (3)多线程中用户 通过互斥机制来同步不同线程对堆的操作
               dwInitialSize是堆在 最开始是commit的内存大小(页大小4KB的倍数)
               dwMaximumSize是堆 可以增长的最大值,若将其 设为0,那么堆可以增长直至用完所有内存

 

4.堆中分配内存的步骤              

              (1)遍历已分配内存和未分配内存的内存块的链表

              (2)找到一块足够大的内存模块

              (3)在找到的内存模块上分配内存,并将其标记为已分配

              (4)在内存链表中加入新的条目

5.Common API:

              HeapCreate             HeapSize HeapReAlloc

              HeapFree                HeapDestroy                           GetProcessHeaps 取得堆的句柄

              HeapValidate           HeapCompact

              HeapLock                HeapUnlock

              HeapWalk

 

 

 

 

堆非常适合分配大量的小型数据。它是用来管理链表和树的最佳方式。但是它分配和释放内存块的速度比虚拟内存和内存映射文件要慢,而且也无法再对物理存储器的调拨和撤销调拨进行直接控制。

一个进程同时可以有多个堆,进程在整个生命周期内可以创建和销毁这些堆。但是,默认的堆是在进程开始运行之前由系统自动创建的,在进程终止后会自动销毁。我们无法销毁进程的默认堆。每个堆都有一个用来标识自己的堆句柄,所有分配和释放内存块的堆函数都会在参数中用到这个堆句柄。

 

我们可以调用如下函数来得到进程默认堆得句柄:

HANDLE WINAPI GetProcessHeap(void);

 

创建额外的堆
HANDLE WINAPI HeapCreate(
  __in          DWORD flOptions,
  __in          SIZE_T dwInitialSize,
  __in          SIZE_T dwMaximumSize
);

flOptions参数用来表示对堆的操作该如何进行。可以指定0, HEAP_NO_SERIALIZE, HEAP_GENERATE_EXCEPTIONS, HEAP_CREATE_ENABLE_EXECUTE。

HEAP_NO_SERIALIZE标志使得多个线程可以同时访问一个堆,这使得堆中的数据可能会遭到破坏,因此应该避免使用

HEAP_GENERATE_EXCEPTIONS标志告诉系统,每当在堆中分配或者重新分配内存块失败的时候,抛出一个异常。

HEAP_CREATE_ENABLE_EXECUTE标志告诉系统,我们想在堆中存放可执行代码。如果不设置这个标志,那么当我们试图在来自堆的内存块中执行代码时,系统会抛出EXCEPTION_ACCESS_VIOLATION异常。

 

dwInitialSize参数表示一开始要调拨给堆的字节数。如果需要,HeapCreate会把这个值向上取整到CPU页面大小的整数倍

 

dwMaximumSize参数表示堆能增长到的最大大小。如果dwMaximumSize 大于0,那么创建的堆会有一个最大大小。

如果dwMaximumSize为0,那么创建的堆将是可增长的,直到用尽所有的物理存储器为止

 

 

从堆中分配内存块
LPVOID WINAPI HeapAlloc(
  __in          HANDLE hHeap,
  __in          DWORD dwFlags,
  __in          SIZE_T dwBytes
);

 

调整内存块的大小
LPVOID WINAPI HeapReAlloc(
  __in          HANDLE hHeap,
  __in          DWORD dwFlags,
  __in          LPVOID lpMem,
  __in          SIZE_T dwBytes
);

dwFlags参数可以是HEAP_GENERATE_EXCEPTIONS,HEAP_NO_SERIALIZE,HEAP_ZERO_MEMORY, HEAP_REALLOC_IN_PLACE_ONLY。

HEAP_ZERO_MEMORY这个标志只有在增大内存块的大小时才有用。在这种情况下,内存中的额外字节会被清零减小内存块的大小时,这个标志不起任何作用

HEAP_REALLOC_IN_PLACE_ONLY在增大内存块的时候,HeapReAlloc可能会在堆内部移动内存块,而这个标志用来告诉HeapReAlloc不要移动内存块。

如果HeapReAlloc函数能在不移动内存块的前提下就能让它增大,那么函数会返回原来的内存块地址。另一方面,如果HeapReAlloc必须移动内存块的地址,那么函数将返回一个指向一块更大内存块的新地址

如果一个内存块是链表或者树的一部分,那么需要指定这个标志。因为链表或者树的其他节点可能包含指向当前节点的指针,把节点移动到堆中其他的地方会破坏链表或树的完整性。

 

lpMemdwBytes参数分别用来指定想要调整大小的内存块的原始地址以及内存块的新大小,以字节为单位。

 

获得内存块的大小

分配一块内存后,可以调用如下函数来得到这块内存的实际大小:

SIZE_T WINAPI HeapSize(
  __in          HANDLE hHeap,
  __in          DWORD dwFlags,
  __in          LPCVOID lpMem
);

参数hHeap用来标识堆

参数dwFlags可以是0或者HEAP_NO_SERIALIZE

参数lpMem标识内存块快的地址

 

释放内存块
BOOL WINAPI HeapFree(
  __in          HANDLE hHeap,
  __in          DWORD dwFlags,
  __in          LPVOID lpMem
);

 

销毁堆
BOOL WINAPI HeapDestroy(
  __in          HANDLE hHeap
);

调用这个函数的时候,系统会释放堆中包含的所有内存块,同时系统会收回堆所占用的物理存储器和地址空间区域

 

其他堆函数

1.ToolHelp函数允许我们枚举进程中的堆以及堆中分配的内存块

Heap32First,Heap32Next, Heap32ListFirst, Heap32ListNext。

 

2.得到进程中所有堆的句柄

DWORD WINAPI GetProcessHeaps(
  __in          DWORD NumberOfHeaps,
  __out         PHANDLE ProcessHeaps
);

用法如下:

HANDLE hHeaps[25];

DWORD dwHeaps = GetProcessHeaps(25, hHeaps);

If(dwHeaps > 25)

{

         //大于25个句柄

}

else

{

         //hHeaps[0]

}

注意:函数所返回的句柄数组中也包括了进程的默认堆句柄。

 

3.验证堆的完整性

BOOL WINAPI HeapValidate(
  __in          HANDLE hHeap,
  __in          DWORD dwFlags,
  __in          LPCVOID lpMem
);

通常调用这个函数的时候, 我们会传入一个堆句柄,和一个标志0,并传NULL给lpMem。

该函数会遍历堆中的各个内存块,确保没有任何一块内存被破坏

 

4.为了让堆中闲置的内存块能重新接合在一起,并撤销调拨给堆中闲置内存块的存储器,可以调用如下函数:

SIZE_T WINAPI HeapCompact(
  __in          HANDLE hHeap,
  __in          DWORD dwFlags
);

一般来说,我们会传0 给dwFlags参数。



1.         堆适合分配小内存块,不需要按分配粒度或者页大小对齐。堆在最初只是预定了一块区域,在客户分配时将预定的区域提交,在客户释放后可能反提交。

2.         关于默认堆:GetProcessHeap返回,用户模式代码无法销毁它,在进程结束后由系统销毁。进程可以通过链接选项“/HEAP:reserve[,commit]”来设置默认堆大小。因为默认堆属于进程,所以在DLL中不应设置该链接选项。WindowsANSIAPIUnicode版转化的时候从默认堆分配字符串缓存,LocalAllocGlobalAlloc也从默认堆分配内存。默认堆对外界访问进行了同步,即没有使用HEAP_NO_SERIALIZE标记。

3.         使用独立堆的一些好处:(1)写堆内存出错后,不会影响其他堆的数据。(2)对特定类型数据使用独立堆的话,由于分配块大小相同,具有速度快、无碎片的优点。(3)相关数据使用独立的堆,在访问这些数据时访问的页面更集中,减少PageFault。(4)对特定线程上的逻辑结构使用独立堆,不必加锁,提高性能。

4.         HeapCreate:参数fdwOption,如果在创建堆的时候指定了部分标志(如HEAP_NO_SERIALIZE标志等),以后每次访问堆这些标志都生效;如果创建的时候没有指定,那后续的每次访问可以单独指定标志。 HEAP_NO_SERIALIZE-访问堆的时候不加锁。HEAP_GENERATE_EXCEPTIONS-分配内存失败的时候抛出异常,默认行为是返回NULLHEAP_CREATE_ENABLE_EXECUTE-可以在堆内存上放置代码来执行。参数dwInitalSize-初始堆大小。参数dwMaximumSize-如果非0,表示如果堆内存使用量达到这个值后再分配会失败;为0,表示堆会自动增大,直到内存用尽。

5.         HeapAlloc、HeapSizeHeapFreeHeapDestroy,容易理解。

6.         HeapReAlloc:HEAP_ZERO_MEMORY-增大内存时,增加的字节初始化为0HEAP_REALLOC_IN_PLACE_ONLY-要求不移动起始地址的情况下改变大小,需要增大时如果当前位置剩余空间不足会返回NULL

7.         HeapSetInformation:标记HeapEnableTerminationOnCorruption-Visita以上使用。默认情况下,堆内存被破坏后只在调试器中触发一个断言然后继续执行,这个标记允许发现堆破坏就抛出异常。该标记影响进程中所有堆,无法清空标记。标记HeapCompatibilityInformation-值为2的时候,表示启用低碎片堆(lowfragmentation heap算法,启用该算法的堆针对内存碎片问题优化有更好的性能。

8.         Heap32ListFirst、Heap32ListNext-遍历快照(CreateToolhelp32Snapshot)中的堆Heap32FirstHeap32Next-遍历指定堆中的块。GetProcessHeaps-获得包括默认堆在内的所有堆句柄。HeapValidate-检查指定堆中所有块或者单个块的有效性。HeapCompact-将堆中闲置块合并,并反提交。HeapLockHeapUnlock-锁定堆。HeapWalk-遍历指定堆中的块,建议先锁堆。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值