前面我们说过堆非常适合分配大量的小型数据。使用堆可以让程序员专心解决手头的问题,而不必理会分配粒度和页面边界之类的事情。因此堆是管理链表和数的最佳方式。但是堆进行内存分配和释放时的速度比其他方式都慢,而且无法对物理存储器的调拨和撤销调拨进行控制。

什么是堆?

在系统内部堆就是一块预定的地址空间区域。刚开始堆的大部分页面都没有调拨物理存储器。随着我们不断的从堆中分配内存,堆管理器会给堆调拨越来越多的物理存储器。这些物理存储器始终是从页交换文件中分配的。释放堆中的内存时,堆管理器会撤销已调拨的物理存储器。

进程默认堆。

进程初始化时,系统会在进程的地址空间中创建一个堆。这个堆被称为进程的默认堆。默认情况下,这个堆的地址空间区域大小是1MB。程序员可以控制这个大小。我们可以在创建应用程序时用/HEAP连接器开关来改变默认堆的大小。由于DLL没有与之关联的堆,因此在创建DLL时,不应该使用/HEAP开关。

  1. /HEAP:reserve[,commit]

由于许多Windows函数会用到进程默认堆,因此对默认堆的访问必须一次进行。系统会保证在任何情况下只让一个线程从默认堆中分配或释放内存块。如果应用程序只有一个线程,而我们又希望以最快的方式访问堆,我们应该创建自己的堆,而不要使用默认堆。

默认堆由系统创建和释放,我们无法销毁默认堆。每个堆都有一个标识自己的句柄,所有分配和释放内存块的堆函数都会在参数中使用到这个堆句柄。我们可以调用GetProcessHeap来得到默认堆的句柄。

  1. HANDLE GetProcessHeap();

创建额外堆的时机:

一:对数据保护。创建两个或多个独立的堆,每个堆保存不同的结构,对两个堆分别操作,可以使问题局部化。

二:更有效的内存管理。创建额外的堆,管理同样大小的对象。这样在释放一个空间后可以刚好容纳另一个对象。

三:内存访问局部化。将需要同时访问的数据放在相邻的区域,可以减少缺页中断的次数。

四:避免线程同步开销。默认堆的访问是依次进行的。堆函数必须执行额外的代码来保证线程安全性。通过创建额外的堆可以避免同步开销。

五:快速释放。我们可以直接释放整个堆而不需要手动的释放每个内存块。这不但极其方便,而且还可以更快的运行。

创建额外的堆

我们可以调用HeapCreate来创建额外的堆:

  1. HANDLE HeapCreate(
  2. DWORD fdwOptions,
  3. SIZE_T dwInitilialize,
  4. SIZE_T dwMaximumSize);

fdwOptions表示对堆的操作该如何进行。可以传入0,   HEAP_NO_SERIALIZE,

HEAP_GENERATE_EXCEPTIONS,HEAP_CREATE_ENABLE_EXECUTE或这些标志的组合。

HEAP_NO_SERIALIZE告诉堆管理器堆管理器不负责堆的线程安全性。对堆的线程安全性的控制由程序员控制。

HEAP_GENERATE_EXCEPTIONS标志告诉系统,每当在堆中分配或重新分配内存块失败时抛出一个异常。用来通知应用程序有错误发生。

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

dwMaximumSize表示堆所能增长到的最大大小。即系统为堆所预定的地址空间的最大大小。如果试图分配的内存块超过最大大小,分配操作会失败。如果dwMaximumSize为0,则表明创建的堆是可增长的,没有一个指定上限。

函数执行成功HeapCreate会返回一个句柄,标识了新创建的堆。

堆创建后,需要从堆中分配内存时,要调用HeapAlloc函数:

  1. PVOID HeapAlloc(
  2. HANDLE hHeap,
  3. DWORD fdwFlags,
  4. SIZE_T dwBytes);

hHeap是一个堆句柄,表示要从哪个堆分配内存。

fdwFlags用来执行一些标志。这些标志会对分配产生一些影响。总共有三个标志:

HEAP_ZERO_MEMORY,HEAP_GENERATE_EXCEPTIONS和HEAP_NO_SERIALIZE。

HEAP_ZERO_MEMORY会让HeapAlloc返回之前把内存块的内容都清0 。

HEAP_GENERATE_EXCEPTIONS用来告诉系统如果堆中没有足够的内存来满足分配请求,此次调用的

HeapAlloc应抛出异常。可以在创建堆时指定这个标志,只要在这个堆上分配内存,如果内存不足都抛出异常。

如果分配成功HeapAlloc会返回内存块地址。否则将会返回NULL。

默认情况下,对堆的访问会依次进行。当任何程序试图从堆中分配一块内存时,HeapAlloc会执行以下操作:

1:遍历已分配的内存的链表和闲置内存的链表。

2:找到一块足够大的闲置内存块。

3:分配一块新的内存,将2找到的内存块标记为已分配。

4:将新分配的内存块添加到已分配的链表中。

注意:在分配大于1MB的内存时应该避免使用堆函数,而应该使用VirtualAlloc函数。

HeapReAlloc可以改变堆中某一块内存的大小:

  1. PVOID HeapReAlloc(
  2. HANDLE hHeap,
  3. DWORD fdwFlags,
  4. PVOID pvMem,
  5. SIZE_T dwBytes);

hHeap用来标识一个堆。

fdwFlags用来在调整内存块大小时用到这些标志。可以有以下标志:HEAP_GENERATE_EXCEPTIONS,HEAP_NO_SERIALIZE,HEAP_ZERO_MEMORY,HEAP_REALLOC_IN_PLACE_ONLY。

前两个标志与前面介绍的一样。只有当增大内存块时HEAP_ZERO_MEMORY才有用。额外的字节会被清0。

在增大内存块时HeapReAlloc可能会移动内存块,HEAP_REALLOC_IN_PLACE_ONLY标志告诉HeapReAlloc尽量不要移动内存块。如果不移动不能增大内存块,则HeapReAlloc返回新地址。

pvMem指定要调整大小的内存块。

dwBytes指定内存块的新大小。

分配一块内存后,调用HeapSize可以获得这块内存的实际大小:

  1. SIZE_T HeapSize(
  2. HANDLE hHeap,
  3. DWORD fdwFlags,
  4. LPCVOID pvMem);

hHeap用来标识堆。

pvMem表示内存地址。

dwFlags可以是0或HEAP_NO_SERIALIZE

当不要使用一块内存时可以调用HeapFree来释放它:

  1. BOOL HeapFree(
  2. HANDLE hHeap,
  3. DWORD fdwFlags,
  4. PVOID pvMem);

如果操作成功则返回TRUE。调用这个函数可能会使堆管理器撤销一些已经调拨的物理存储器。

如果应用程序不再需要自己创建的堆,可以调用HeapDestroy来销毁它:

  1. BOOL HeapDestroy(HANDLE hHeap);

此时系统会收回堆所占用的物理存储器和地址空间区域。执行成功则返回TRUE。如果我们不调用此函数主动销毁自己创建的堆,在进程结束时,系统会替我们销毁。我们不能调用此函数销毁默认堆,默认堆由系统管理。

在C++中使用堆

在C++中我们可以调用new操作符来分配类对象。不需要时可以调用delete来释放它。如

  1. CA *pCA=new CA;

在编译此段代码时,编译器会首先检查类CA是否重载了new操作符成员函数。如果找到编译器会调用这个函数。否额,会调用C++标准的new操作符。

  1. deleted pCA;

对此句代码C++编译器会执行与上面类似的步骤,只有CA类没有重载delete操作符成员函数时,才会调用标准的C++delete运算符。

通过对C++类的new和delete操作符进行重载,我们可以非常容易的将堆函数加以运用:

  1. class CA
  2. {
  3. public:
  4. CA();
  5. ~CA();
  6. public:
  7. void *operator new(size_t size);
  8. void*operator delete(void*p);
  9. };

上述代码调用operator new和operator delete是从默认堆中分配的内存。我们可以让其在自己创建的堆中分配内存,一般让所有对象共享同一个堆,每个对象都创建一个堆为导致额外的性能开销。可以采用计数法来对堆的生存期进行控制。

ToolHelp函数允许我们枚举进程的堆以及分配的内存块。它包括一下函数:Heap32First,Heap32Next,Heap32ListFirst和Heap32ListNext。

由于进程在自己的地址空间可以有多个堆,GetProcessHeaps可以让我们得到这些堆的句柄。

  1. DWORD GetProcessHeaps(
  2. DWROD dwNumHeaps,
  3. PHANDLE phHeaps);

phHeaps是一个数组指针。用以存储返回的堆句柄。

dwNumHeaps是数组大小。

函数返回句柄数组个数。

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

  1. HANDLE hHeaps[20];
  2. DWORD dwHeaps=GetProcessHeaps(20,hHeaps);

HeapValidate可以验证堆的完整性。

  1. BOOL HeapValidate(
  2. HANDLE hHeap,
  3. DWORD fdwFlags,
  4. LPCVOID pvMem);

通常在调用这个函数时,我们会传一个堆句柄和一个标志0,并传入NULL给pvMem。该函数会遍历堆中的各个内存块,确保没有任何一块内存被破坏。如果给pvMem制定一块内存地址,那么函数就只检查这一块内存的完整性。

为了让堆中闲置的内存块能重新结合在一起,并撤销调拨给堆中闲置内存块的存储器,可以调用HeapCompact:

  1. UINT HeapCompact(
  2. HANDLE hHeap,
  3. DWORD fdwFlags);

一般来说会传0给fdwFlags。

下面两个函数要配对使用,用于线程同步:

  1. BOOL HeapLock(HANDLE hHeap);
  2. BOOL HeapUnlock(HANDLE hHeap);

当第一个线程调用HeapLock时,它就占有了堆。其他线程在调用堆函数时,系统就会暂停其他线程。只有当第一个线程调用HeapUnlock之后才会唤醒被暂停的进程。

HeapAlloc,HeapSize,HeapFree这些函数会在内部调用HeapLock和HeapUnlock,一般来说不需要自己去调用HeapLock和HeapUnlock。

最后一个函数是HeapWalk,它允许我们遍历堆的内容。只用于调试。具体不再介绍。

以上参考自《Windows核心编程》第五版第三部分,如有纰漏,请不吝指正!!

《Windows核心编程系列》十四谈谈默认堆和自定义堆的更多相关文章

  1. 《windows核心编程系列》四谈谈进程的建立和终止

    第二部分:工作机理 第一章:进程 上一章介绍了内核对象,这一节开始就要不断接触各种内核对象了.首先要给大家介绍的是进程内核对象.进程大家都不陌生,它是资源和分配的基本单位,而进程内核对象就是与进程相关 ...

  2. 《windows核心编程系列》十七谈谈dll

    DLL全称dynamic linking library.即动态链接库.广泛应用与windows及其他系统中.因此对dll的深刻了解,对计算机软件开发专业人员来说非常重要. windows中所有API ...

  3. 《windows核心编程系列》二谈谈ANSI和Unicode字符集 .

    http://blog.csdn.net/ithzhang/article/details/7916732转载请注明出处!! 第二章:字符和字符串处理 使用vc编程时项目-->属性-->常 ...

  4. 《windows核心编程系列 》六谈谈线程调度、优先级和关联性

    线程调度.优先级和关联性 每个线程都有一个CONTEXT结构,保存在线程内核对象中.大约每隔20ms windows就会查看所有当前存在的线程内核对象.并在可调度的线程内核对象中选择一个,将其保存在C ...

  5. 《Windows核心编程系列》十一谈谈Windows线程池

    Windows线程池 上一篇博文我们介绍了IO完成端口.得知IO完成端口可以非常智能的分派线程.但是IO完成端口仅对等待它的线程进行分派,创建和销毁线程的工作仍然需要我们自己来做. 我们自己也可以创建 ...

  6. 《Windows核心编程系列》八谈谈用内核对象进行线程同步

    使用内核对象进行线程同步. 前面我们介绍了用户模式下线程同步的几种方式.在用户模式下进行线程同步的最大好处就是速度非常快.因此当需要使用线程同步时用户模式下的线程同步是首选. 但是用户模式下的线程同步 ...

  7. 《windows核心编程系列》七谈谈用户模式下的线程同步

    用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...

  8. 《windows核心编程系列》五谈谈线程基础

    线程基础 与前面介绍的进程一样,线程也有两部分组成.一个是线程内核对象.它是一个数据结构,操作系统用它来管理线程以及用它来存储线程的一些统计信息.另一个是线程栈,用于维护线程执行时所需的所有函数参数和 ...

  9. 《Windows核心编程系列》十三谈谈在应用程序中使用虚拟内存

    在应用程序中使用虚拟内存 Windows提供了以下三种机制对内存进行操控: 一:虚拟内存.最适合来管理大型对象数据或大型结构数组. 二:内存映射文件.最适合用来管理大型数据流,以及在同一机 器上运行的 ...

  10. 《Windows核心编程系列》九谈谈同步设备IO与异步设备IO之同步设备IO

    同步设备IO 所谓同步IO是指线程在发起IO请求后会被挂起,IO完成后继续执行. 异步IO是指:线程发起IO请求后并不会挂起而是继续执行.IO完毕后会得到设备的通知.而IO完成端口就是实现这种通知的很 ...

随机推荐

  1. java学习——关于搜索异常处理的总结

    根据网上的资料可以知道,异常处理是为了检测到程序运行中发生的非正常情况的检测而设立的一种机制,异常的英文单词是exception,字面翻译就是“意外.例外”的意思,也就是非正常情况.关于平常我们经常遇 ...

  2. Spring Boot项目错误:Error parsing lifecycle processing instructions

    pom.xml文件错误:Error parsing lifecycle processing instructions 解决方法:清空.m2/repository下的所有依赖文件,重新下载即可解决该问 ...

  3. Java 函数式接口 lambda

    import java.io.Serializable; import java.util.ArrayList; import java.util.List; public class Demo1 { ...

  4. Go---设计模式(策略模式)

    策略模式定义了算法家族,在调用算法家族的时候不感知算法的变化,客户也不会受到影响. 下面用<大话设计模式>中的一个实例进行改写. 例:超市中经常进行促销活动,促销活动的促销方法就是一个个策 ...

  5. jquery在ajax新加入的元素后绑定事件click

    使用YII在做一个点击小图.能够在弹出窗体中显示大图的功能的时候,发现.GridView首页面的列表项按点击时一切正常,但按下了下一页后. 再点击小图,就不起作用了.原来,这是GridView使用了a ...

  6. VC++如何折叠代码

    工具-选项,然后在文本编辑器,C/C++中的格式设置,把大纲语句块设置为True   这样之后,还是不能像C#一样使用region折叠代码,但是可以方法和if语句都会自动显示可以折叠.   使用#pr ...

  7. cocos2d-x 2.0下怎样让BOX2D DEBUG DRAW的方法笔记

    原文链接: 这两天玩 cocos2d-x 和 box2d,发现 cocos2d-x 2.0 版本号要使用老方法 debug 渲染会出错.于是找到了新方法来 debug draw: 首先在你的头文件中添 ...

  8. vue 定义全局函数

    方法一:main.js 注入 (1)在main.js中写入函数 Vue.prototype.changeData = function (){ alert('执行成功'); } (2)在所有组件里可调 ...

  9. List<Guid?> a = new List<Guid?>();

    正常写法 泛型 类型 为 Guid? List<Guid?> a = new List<Guid?>(); Guid? b = null; Nullable<Guid&g ...

  10. HDU 5302(Connect the Graph- 构造)

    Connect the Graph Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others ...