一、为什么要使用堆 (Heap)?

应用程序虽然可以使用页面粒度的函数(如VirualAlloc)来分配一个最小为4KB8K的内存块,但是很多时候我们并不需要分配这么大的内存块,我们可能只想分配 1K,2K 的内存块,那么这个时候无论从内存的使用率,还是从性能的角度来看,再分配这么大的一个内存区域显然不是最优的了。

为了满足这种需求,Windows 提供了一个被称为“堆管理器”的组件,它负责管理大内存区域中的内存分配,这些大内存区域就是通过一些页面粒度的内存分配函数(如VirualAlloc)来预定(reserve)的。

堆管理器中的分配粒度相对比较小:在32位系统上是8字节,在64位系统上是16字节

堆管理器已经被 Windows 系统精心设计,在这些小内存分配的情况下会进行内存使用率和性能两个方面的优化。

二、进程的默认堆

每个进程至少有一个堆,那就是进程的默认堆。进程的默认堆是在进程启动的时候创建的,而且在进程的生命周期中永远不会被删除。

“默认堆”的默认大小为1MB,但是可以通过/HEAP链接器编译器选项来指定一个更大的起始大小。这个大小值只是初始的保留内存大小,后期根据需要它可以自动扩充。

应用程序可以调用GetProcessHeap来获取进程的默认堆,也可以通过调用HeapCreate函数来创建额外的私有堆,当一个进程不在需要一个私有堆的时候,它可以调用HeapDestory来释放虚拟地址空间。

三、crt 堆

C 语言的malloc,free函数以及 C++的new,delete都是从上分配和释放内存的。但是他们所使用的堆不是进程的默认堆,他们使用的是私有堆。可是我们在使用malloc函数之前并有进行任何私有堆的创建操作呀? 因为malloc函数使用的这个私有堆不需要程序员来创建,而是在 C 或 C++运行时库 DLL 的启动代码_DllMainCRTStartup中自动创建的。下面通过解析malloc函数的调用过程来说明这一点。

malloc函数的定义在malloc.c文件中,调用流程如下:
(以Microsoft Visual Studio 10.0为例,malloc.c文件路径为C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\crt\src

1
2
3
(1). void* __cdecl malloc(size_t const size)
(2). void * __cdecl _malloc_base (size_t size)
(3). void * __cdecl _heap_alloc (size_t size)

_heap_alloc 的定义如下:

1
2
3
4
5
6
7
8
9
10
__forceinline void * __cdecl _heap_alloc (size_t size)
{
if (_crtheap == 0) {
_FF_MSGBANNER(); /* write run-time error banner */
_NMSG_WRITE(_RT_CRT_NOTINIT); /* write message */
__crtExitProcess(255); /* normally _exit(255) */
}

return HeapAlloc(_crtheap, 0, size ? size : 1);
}

从上面的代码中,我们可以看到分配内存块的时候使用的是_crtheap句柄标记的堆。那么_crtheap堆是何时创建的了?

我们从heapinit.c文件中的_heap_init函数可以看到_crtheap堆的创建过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
HANDLE _crtheap=NULL;

/***
*_heap_init() - Initialize the heap
*
*Purpose:
* Setup the initial C library heap.
*
* NOTES:
* (1) This routine should only be called once!
* (2) This routine must be called before any other heap requests.
*
*Entry:
* <void>
*Exit:
* Returns 1 if successful, 0 otherwise.
*
*Exceptions:
* If heap cannot be initialized, the program will be terminated
* with a fatal runtime error.
*
*******************************************************************************/

int __cdecl _heap_init (void)
{
ULONG HeapType = 2;

// Initialize the "big-block" heap first.
if ( (_crtheap = HeapCreate(0, BYTES_PER_PAGE, 0)) == NULL )
return 0;

#ifdef _WIN64
// Enable the Low Fragmentation Heap by default on Windows XP and
// Windows Server 2003. It's the 8 byte overhead heap, and has
// generally better performance charateristics than standard heap,
// particularly for apps that perform lots of small allocations.

if (LOBYTE(GetVersion()) < 6)
{
HeapSetInformation(_crtheap, HeapCompatibilityInformation,
&HeapType, sizeof(HeapType));
}
#endif /* _WIN64 */
return 1;
}

从上面的代码,我们可以看到,创建的私有堆句柄存放在一个全局的_crtheap变量中,后面每次调用malloc函数都是从该堆分配内存块。

四、Win32 堆函数

我们最常用的 Windows 堆函数如下:

  • HeapCreateHeapDestory — 创建或删除一个私有堆
  • HeapAlloc — 分配一个堆内存块
  • HeapFree — 释放一个原先由HeapAlloc分配的内存块
  • HeapReAlloc — 增长或缩减一个已分配的内存块的大小
  • HeapLockHeapUnLock — 控制堆操作的内存访问
  • HeapWalk — 列举一个堆内部的内存项和区域。

五、Windows 内存管理 API 分层结构

从上图可以看到,虚拟内存机制(Virtual Memory)是 windows 内存体系的基础,无论你是使用,还是使用内存映射文件,它们的底层都是基于虚拟内存来实现的。

从上往下,每一层的 API 在内部会依次调用下一层的 API。下图中列举了每层 API 中经常使用的函数:

  • CRT Memory Functions:malloc, free, new, delete
  • Local, Global Memory API: ** LocalAlloc, GlobalAlloc这 2 个函数现在不建议使用,注意是为了兼容以前的老代码才保留下来的**)
  • Heap Memory API:HeapCreate, HeapAlloc, HeapDestory
  • Virtual Memory API:VirtualAlloc, VirtualFree
  • Memory Mapped File API:CreateFileMapping, MapViewOfFile, MapViewOfFileEx, UnMapViewOfFile

文章图片带有“CSDN”水印的说明:
由于该文章和图片最初发表在我的CSDN 博客中,因此图片被 CSDN 自动添加了水印。