第18章 堆

18.1 进程的默认堆

(1)堆的特点

  ①不必考虑分配粒度和页面边界问题,但分配和释放内存的速度比其他方式慢

  ②堆是系统从页交换文件中预订的一块地址空间,系统会负责调拨和撤销调拨物理存储器。

(2)进程默认堆

  ①进程初始化时,系统会在进程地址空间中一个特殊的区域,这个区域为进程的默认堆(默认为1MB),也可以使用/HEAP链接器开关改变默认区域的大小,使用方法为/HEAP:reserve,[commit]

  ②许多Windows函数要用到默认堆,如ANSI版的Windows函数在底层必须先把ANSI字符串转为Unicode,再调用其Unicode版本(因为内核是Unicode版的)

  ③默认堆是进程私有的,不能为其他进程所共享。GlobalAlloc\LocalAlloc函数也是在默认堆是分配内存的。在使用剪贴板时要用GlobalAlloc从默认堆分配内存,在调用SetClipboardData时,系统会将该内存映射到进程高地址(内核分区),以便让其他进程可共享,这也就是调用GlobalAlloc时要指定为GMEM_MOVABLE的原因。

  ④对进程的默认堆的访问是串行化的。两个线程不能同时访问,只能等另一个线程结束对默认堆的访问之后才能访问(这可以防止堆被错误的分配和释放)。

  ⑤进程可以有多个堆,但默认堆只有一个。且默认堆的生命周期与进程一样,进程开始时由系统自动创建,进程结束时自动销毁。

(3)获取默认堆句柄的方法:HANDLE GetProcessHeap();

18.2 为什么要创建额外的堆

(1)对组件进行保护:(如图1所示)

  ①假设链表代表一个缺陷,不小心覆盖了NODE1后面的8个字节,从而破坏了BRANCH3结构体中的数据。BinTree.cpp中的代码后来在遍历二叉树时,可能因这个原因而失败。

  ②以上的错误,会造成一种假象,好象是BinTree.cpp代码出现错误,而实际上是ListList.cpp的代码有缺陷,这种错误很难跟踪与定位。

  ③可以创建两个独立的堆,一个用来保存Node结构,另一个用来保存Branch结构,这样可以使问题局部化。

(2)更有效的内存管理(如图2所示)

  ①假NODE结构为24字节,BRANCH为32字节,此时正好占满整个堆。如果释放NODE2和NODE4虽然可以回收48字节的空间,但会出现内存碎片,现在我们需要分配一个BRANCH结构时会出现失败。

  ②如果创建两个堆,每个堆只包含同样大小的对象,则可以避免上述现象。

(3)使内存访问局部化

  ①如果把内存的访问局限在一个较小的地址区间内,可以减少内存和磁盘之间的页面换入和换出操作,提高性能。

  ②程序设计时,可以在同一个堆中相邻的内存地址分配NODE对象,这样就可以尽可能把多个NODE对象放在同一个物理内存页中,遍历链表时,就可以减少访问太多的不同页面而导致页面的换入换出的交换操作。

  ③如果在同一个堆中分配了NODE和BRANCH对象且各个NODE对象都不相邻,极端情况下,设每个内存页只有一个NODE对象和BRANCH对象,这时遍历链表时可能会导致访问每个NODE都会引起页面错误,效率极低。

(4)避免线程同步的开销

  ①默认下对堆的访问是依次进行的。这样即使在同一时刻有多个线程要访问堆,也不会出现数据被破坏的情况,但堆函数要执行额外的堆的线程安全性保护的代码。

  ②如果创建一个新的堆,且该堆只会有一个线程会对其访问,这里可以给堆指定HEAP_NO_SERIALIZE属性,这样堆函数就不需执行额外的保护代码,从而提高了速度。

(5)快速释放

  把一些数据结构存入一个专门的堆时,在不需要这些结构时,可以不必显式地释放堆中的每个内存块。而是可以直接销毁整个堆。

18.3 如何创建额外的堆

(1)创建私有堆的函数:HANDLE HeapCreate(fdwOptions,dwInitialSize,dwMaximumSize)


参数


含义


DWORD fdwOptions


新堆的可选属性,可以是下列的组合

①0:默认

②HEAP_NO_SERIALIZE:非独占地访问堆,不需要串行化。不指定该标志时,就是默认的独占访问。(该标志是线程不安全,不建议使用!),

③HEAP_GENERATE_EXCEPTIONS:当在堆中分配或重新分配内存块失败时,执出一个异常,用于通知应用程序有错误发生。

③HEAP_CREATE_ENABLE_EXCUTE:在堆中存放可执行代码,但需要在“数据执行保护”选项中启用DEP(详细可参考第13章)


SIZE_T dwInitialSize


初始化时,要调拨给堆的字节数。函数会将该值向上取整到CPU页面大小的整数倍。


SIZE_T dwMaximumSize


堆所能增长到的最大大小。如果设为0,表示没有上限。从堆中分配内存会使堆用尺所有的物理存储器为止。


返回值


返回新创建的私有堆的句柄。


备注:①默认下,调用Heap*函数,如果操作系统发现堆被破坏(如写内存时越界),这时在调试运行时会引发一个断言,但没有其他信息。

②可以在堆管理器中进行设置,一旦Heap*函数发现堆破坏,就抛出一个异常,方法如下:

HeapSetInformation(NULL,HeapEnableTerminationOnCorruption,NULL,0)。这个策略会应用到进程中所有的堆。而且一旦为进程所有的堆启用这个特性,就再也无法禁用它了。

(2)从堆中分配内存块:PVOID HeapAlloc(hHeap,fdwFlags,dwBytes)


参数


含义


HANDLE hHeap


堆的句柄,表示要从哪个堆中分配内存。


DWORD fdwFlags


HEAP_ZERO_MEMORY:把内存块内容清零

HEAP_GENERATE_EXECPTIONS:如果堆中没有足够内存,函数会抛出异常。如果内存不足时,会抛出STATUS_NO_MEMORY异常;如果堆被破坏或传入的参数不正确时,会抛出STATUS_ACCESS_VIOLATION异常。

HEAP_NO_SERIALIZE:强制系统不要把这次的HeapAlloc调用与其他线程对同一个堆的访问串行化。(可能破坏堆的完整性,慎用!)


SIZE_T dwBytes


要从堆中分配多少个字节


返回值


返回分配到的内存地址。


备注:①调用HeapCreate时可以传入HEAP_GENERATE_EXCEPTIONS标志,这时HeapAlloc可以不指定。如果在创建堆的时候没有指定这个标志,而是在调用HeapAlloc时指定的话,则这个标志只影响当前这次调用,而不会影响在这个堆上所有其他对HeapAlloc函数的调用。

②在分配大块内存(1MB或更多)时,应避免使用堆函数,建议使用VirtualAlloc函数。

③如果分配大小不同的内存块,可能很容易产生地址空间碎片化,我们可以强制系统在分配内存时使用一种低碎片堆的算法。(如果堆使用的是HEAP_NO_SERIALIZE创建,下列调用会失败)

ULONG iValue = 2;

HeapSetInformation(hHeap,HeapCompatibilityInformation,&iVlaue,sizeof(iValue));

(3)调整内存块的大小:PVOID HeapReAlloc(hHeap,fdwFlags,pvMem,dwBytes)


参数


含义


HANDLE hHeap


需要调整大小的内存块所在的


DWORD fdwFlags


HEAP_GENERATE_EXCEPTIONS、HEAP_NO_SERIALIZE

HEAP_ZERO_MEMORY:增大内存块是,额外的字节清0

HEAP_REALLOC_IN_PLACE_ONLY:增大内存块时,不会移动内存块。(这对于链表或树来说,有时很重要,因为节点可能包含指向当前节点指针,当被移到其他堆的其他地方时会破坏链表或树的完整性。


PVOID pvMem


要调整大小的内存块的当前地址


SIZE_T dwBytes


新的内存块的大小


返回值


返回新创建新的内存块的地址或NULL。


备注:如果不需要移动内存块的前提下增大内存块或把内存块减小时,函数会返回原来内存块的地址。如果必须移动内存块,函数会返回一个新的地址。

(4)获得内存块的大小:SIZE_T HeapSize(hHeap,fdwFlags,pvMem);//其中hHeap用来标识堆,参数pvMem表示内存块的地址。fdwFlags为0或HEAP_NO_SERIALIZE。

(5)释放内存块:BOOL HeapFree(hHeap,fdwFlags,pvMem);//各参数与HeapSize含义一样。这个函数可能会使堆管理器撤销一些己经调拨的物理存储器,但这并不是一定的。

(6)销毁堆:BOOL HeapDestroy(hHeap);

  ①该函数会释放堆中所有的内存块,同时回收占用的物理存储器和地址空间。

  ②进程的默认堆在进程结束时会自动销毁,如果手动调用来销毁则函数的调用会被忽略并返回FALSE

  ③其他私有堆在不用时,可以手动调用该函数来销毁。如果没有被销毁,在进程结束以后系统会替我们销毁。

时间: 2024-12-15 23:49:51

第18章 堆的相关文章

《Cracking the Coding Interview》——第18章:难题——题目10

2014-04-29 04:22 题目:给定一堆长度都相等的单词,和起点.终点两个单词,请从这堆单词中寻找一条变换路径,把起点词变成终点词,要求每次变换只能改一个字母. 解法:Leetcode中有Word Ladder,这题基本思路一致. 代码: 1 // 18.10 Given a list of words, all of same length. Given a source and a destionation words, you have to check if there exis

《Cracking the Coding Interview》——第18章:难题——题目9

2014-04-29 04:18 题目:有一连串的数被读入,设计一个数据结构,能随时返回当前所有数的中位数. 解法:用一个大顶堆,一个小顶堆将数分成数量最接近的两份,就能轻松得到中位数了. 代码: 1 // 18.9 A stream of integers are passed to you, you have to tell me the median as they keep coming in. 2 #include <climits> 3 #include <iostream&

《数据结构与算法分析:C语言描述》复习——第五章“堆”——二叉堆

2014.06.15 22:14 简介: 堆是一种非常实用的数据结构,其中以二叉堆最为常用.二叉堆可以看作一棵完全二叉树,每个节点的键值都大于(小于)其子节点,但左右孩子之间不需要有序.我们关心的通常只有堆顶的元素,而整个堆则被封装起来,保存在一个数组中. 图示: 下图是一个最大堆: 实现: 优先队列是STL中最常用的工具之一,许多算法的优化都要利用堆,使用的工具就是优先队列.STL中的优先队列通过仿函数来定义比较算法,此处我偷懒用了“<”运算符.关于使用仿函数的好处,我之后如果有时间深入学习S

《TCP/IP详解卷1:协议》第17、18章 TCP:传输控制协议(2)-读书笔记

章节回顾: <TCP/IP详解卷1:协议>第1章 概述-读书笔记 <TCP/IP详解卷1:协议>第2章 链路层-读书笔记 <TCP/IP详解卷1:协议>第3章 IP:网际协议(1)-读书笔记 <TCP/IP详解卷1:协议>第3章 IP:网际协议(2)-读书笔记 <TCP/IP详解卷1:协议>第4章 ARP:地址解析协议-读书笔记 <TCP/IP详解卷1:协议>第5章 RARP:逆地址解析协议-读书笔记 <TCP/IP详解卷1:协

设计模式之第18章-观察者模式(Java实现)

设计模式之第18章-观察者模式(Java实现) 话说曾小贤,也就是陈赫这些天有些火,那么这些明星最怕的,同样最喜欢的是什么呢?没错,就是狗仔队.英文的名字比较有意思,是paparazzo,这一说法据说来自意大利电影<滴露牡丹开>中一个专门偷拍明星照片的一个摄影师的名字,“Paparazzo”,中文译为帕帕拉齐,俗语就是狗仔队.这些明星因狗仔队而荣,获得曝光率,也因狗仔队而损,被曝光负面新闻,不管怎么说,总之是“火起来了”,让明星们又爱又恨.(众人:鱼哥,你扯远了).咳咳,这个狗仔队其实嘛,也就

《Cracking the Coding Interview》——第18章:难题——题目13

2014-04-29 04:40 题目:给定一个字母组成的矩阵,和一个包含一堆单词的词典.请从矩阵中找出一个最大的子矩阵,使得从左到右每一行,从上到下每一列组成的单词都包含在词典中. 解法:O(n^3)级别的时间和空间进行动态规划.这道题目和第17章的最后一题很像,由于这题的时间复杂度实在是高,我动手写了字典树进行加速.如果单纯用哈希表来作为词典,查询效率实际会达到O(n)级别,导致最终的算法复杂度为O(n^4).用字典树则可以加速到O(n^3),因为对于一个字符串"abcd",只需要

《Cracking the Coding Interview》——第18章:难题——题目11

2014-04-29 04:30 题目:给定一个由'0'或者'1'构成的二维数组,找出一个四条边全部由'1'构成的正方形(矩形中间可以有'0'),使得矩形面积最大. 解法:用动态规划思想,记录二维数组每个元素向上下左右四个方向各有多少个连续的'1',然后用O(n^3)时间计算出满足条件的最大正方形.时间复杂度O(n^3),空间复杂度O(n^2). 代码: 1 // 18.11 Given an NxN matrix of 0s and 1s, find out a subsquare whose

《Cracking the Coding Interview》——第18章:难题——题目12

2014-04-29 04:36 题目:最大子数组和的二位扩展:最大子矩阵和. 解法:一个维度上进行枚举,复杂度O(n^2):另一个维度执行最大子数组和算法,复杂度O(n).总体时间复杂度为O(n^3),还需要O(n)额外空间. 代码: 1 // 18.12 Given an n x n matrix, find the submatrix with largest sum. Return the sum as the result. 2 #include <algorithm> 3 #inc

《Cracking the Coding Interview》——第18章:难题——题目6

2014-04-29 02:27 题目:找出10亿个数中最小的100万个数,假设内存可以装得下. 解法1:内存可以装得下?可以用快速选择算法得到无序的结果.时间复杂度总体是O(n)级别,但是常系数不小. 代码: 1 // 18.6 Find the smallest one million number among one billion numbers. 2 // Suppose one billion numbers can fit in memory. 3 // I'll use quic