第十八章 堆
本章内容
18.1 进程的默认堆
18.2 为什么要创建额外的堆
18.3 如何创建额外的堆
18.4 其他堆函数
与虚拟内存和内存映射文件相比,堆非常适合大量的小型数据。堆是用来管理链表和树的最佳方式。
堆能让我们专心心解决问题,不必理会分配粒度和页面边界的这类很底层的事情。堆的缺点是分配和释放速度比其他方式慢,而且也无法对物理存储器的调拨和撤销进行直接控制。
系统内部,堆就是一块预定的地址空间区域。刚开始区域内的大部分页面都没有调拨物理存储器。随着我们不断从堆中分配内存,堆管理器会给堆调拨越来越多的物理存储器。这些物理存储器始终是从页交换文件中分配的。释放堆中的内存块时,堆管理器会撤销已调拨的物理存储器。
MS并未公开堆的调拨和撤销调拨等规则。如果对性能要求极高需要自己直接操作,应该使用VirtualAlloc和VirtualFree。
18.1 进程的默认堆
进程初始化的时候,系统会在其地址空间创建一个默认堆。这个堆的地址空间区域大小是1MB。但是系统可以增大进程的默认堆,使它大于1MB。也可以使用/HEAP连接器开关来控制默认堆的大小
由于DLL没有与之关联的堆,在创建DLL时不应该使用/HEAP开关
/HEAP:reserve[,commit]
另外windows16下的LocalAlloc和GlobalAlloc二回从进程默认堆中分配内存。
windows保证默认堆的访问是线程安全的(一个线程在访问,另一个试图访问的线程会被挂起等待)。这可能会对性能产生轻微的影响。
如果应用程序只有一个线程,而又希望以最快的速度访问堆,可以创建自己的堆而不使用进程的默认堆。遗憾的是无法控制windows函数来使用自己创建的堆(只能使用默认堆)
默认堆在进程创建的时候创建的,在进程终止后自动销毁。
WINBASEAPI
HANDLE
WINAPI
GetProcessHeap(
VOID
);
18.2 为什么要创建额外堆
1)对组件进行保护
2)更有效的内存管理
3)局部访问
4)避免线程同步开销
5)快速释放
18.2.1 对组件进行保护
例如应用程序使用一个链表和二叉树的数据结构,如果使用一个堆。那么堆中就会混着链表和二叉树的数据。如果由于其中一个数据结构存在代码缺陷会可能会影响到另一个数据结构。这样问题不方便进行排查。
通过创建两个独立堆,就可以使问题局部化。链表中的缺陷不会破坏二叉树的完整性,反之亦然。
18.2.3 更有效的内存管理
如果始终从堆中分配同样大小的对象,我们就可以对它进行更有效的管理。例如每个NODE结构24字节,每个BRANCH结构需要32字节。所有对象从一个堆中分配。如果释放了NODE2和NODE4堆中将出现碎片。这时如果要创建一个新的BRANCH结构即使堆存在48字节的可用空间,分配操作仍然会失败。
如果每个堆只包含同样大小的对象,那么释放一个对象可以保证释放出的空间刚好能够容纳另一个对象。
18.2.3 使内存访问局部化。
当系统必须把一个内存页换出到页交换文件,或者把交换文件中的一个页面换入到内存的时候,对性能产生非常大的影响。
如果把内存访问局限在一个较小的地址区间内,将降低系统需要在内存和磁盘之间进行交换的可能性。
因此在设计的时候,一种很好的做法是把需要同时访问的对象分配在相邻的内存地址。
一种很好的做法是把需要同时访问的对象分配在相邻的内存地址。
如果一个对象的数据分散最差的情况下,每个内存页面中只有一个NODE对象,这种情况下,遍历链表的每个NODE都会引起错误,从而导致整个遍历过程极其缓慢。
18.2.4 避免线程同步的开销
18.2.5 快速释放
18.3 如何创建额外的堆
WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
HeapCreate(
_In_ DWORD flOptions,
_In_ SIZE_T dwInitialSize,
_In_ SIZE_T dwMaximumSize
);
18.3.1 从堆中分配内存块
WINBASEAPI
_Ret_maybenull_
_Post_writable_byte_size_(dwBytes)
LPVOID
WINAPI
HeapAlloc(
_In_ HANDLE hHeap,
_In_ DWORD dwFlags,
_In_ SIZE_T dwBytes
);
Heap是堆的句柄。
ULONG HeapInformationValue = 2;
if (HeapSetInformation(
hHeap, HeapCompatibilityInformation,
&HeapInformationValue, sizeof(HeapInformationValue))){
// hHeap is turned into a low fragmentation heap
}
else {
// hHeap can't be turned into a low fragementation heap.
// Maybe because it has been created with the HEAP_NO_SERIALIZE flag
}
例如把GetProcessHeap返回值传给HeapSetInformation ,默认堆就好变成一个低碎片堆。 如果传入堆句柄是用HEAP_NO_SERIALIZE创建的,那么HeapSetInformation会调用失败。如果代码是在调试模式下运行,有些代码会诅咒堆变成一个低碎片堆。把环境变量_NO_DEBUG_HEAP设为1,就可以关闭这些堆调试选项。
18.3.2 调整内存块的大小
WINBASEAPI
_Success_(return!=0)
_Ret_maybenull_
_Post_writable_byte_size_(dwBytes)
LPVOID
WINAPI
HeapReAlloc(
_Inout_ HANDLE hHeap,
_In_ DWORD dwFlags,
_Frees_ptr_opt_ LPVOID lpMem,
_In_ SIZE_T dwBytes
);
hHeap表示调整大小的内存块属于哪个堆。
18.3.3 获得内存块的大小
WINBASEAPI
SIZE_T
WINAPI
HeapSize(
_In_ HANDLE hHeap,
_In_ DWORD dwFlags,
_In_ LPCVOID lpMem
);
hHeap标识堆
18.3.4 释放内存块
WINBASEAPI
_Success_(return != FALSE)
BOOL
WINAPI
HeapFree(
_Inout_ HANDLE hHeap,
_In_ DWORD dwFlags,
__drv_freesMem(Mem) _Frees_ptr_opt_ LPVOID lpMem
);
dwFlags是0 或 HEAP_NO_SERIALZE
18.3.5 销毁堆
WINBASEAPI
BOOL
WINAPI
HeapDestroy(
_In_ HANDLE hHeap
);
该函数会释放堆中保护的所有内存块,同时系统会回收堆所占用的物理存储器和地址空间区域。
18.3.6 在C++中使用堆
CSomeClass * pSomeClass = new CSomeClass;
系统会检查是否重载了new操作符,如果没有就用c++默认的new操作符。
delete pSomeClass;
通过重载new 和delete就可以将堆函数加以运用。
class CSomeClass {
private:
static HANDLE s_hHeap; // store the handle of the heap
static UINT s_uNumAllocsInHeap; // counter to record the number of instances of CSomeClass
// Other private data and member functions
// ...
public:
void * operator new (size_t size);
void operator delete (void * p);
// other public data and member function
// ...
};
cpp文件
HANDLE CSomeClass::s_hHeap = NULL;
UINT CSomeClass::s_uNumAllocsInHeap = 0;
void * CSomeClass::operator new (size_t size){
if (s_hHeap = NULL) {
// Heap does not exist; create it.
s_hHeap = HeapCreate(HEAP_NO_SERIALIZE, 0, 0);
if (s_hHeap == NULL)
return NULL;
}
// The heap exists for CSomeClass objects.
void *p = HeapAlloc(s_hHeap, 0, size);
if (p != NULL) {
// Memory was allocated successfully; increment
// the count of CSomeClass objects in the heap.
s_uNumAllocsInHeap++;
}
// Return the address of the allocated CSomeClass object.
return p;
}
void CSomeClass::operator delete (void *p) {
if (HeapFree(s_hHeap, 0, p)) {
// Object was deleted successfully.
s_uNumAllocsInHeap--;
}
if (s_uNumAllocsInHeap == 0) {
// If there are no more objects in the heap,
// destroy the heap.
if (HeapDestroy(s_hHeap)) {
// Set the heap handle to NULL so that the new operator
// will know to create a new heap if a new CSomeClass
// object is created.
s_hHeap = NULL;
}
}
}
18.4 其他堆函数
WINBASEAPI
BOOL
WINAPI
HeapValidate(
_In_ HANDLE hHeap,
_In_ DWORD dwFlags,
_In_opt_ LPCVOID lpMem
);
WINBASEAPI
SIZE_T
WINAPI
HeapCompact(
_In_ HANDLE hHeap,
_In_ DWORD dwFlags
);
让堆中闲置的内存块能连续在一起并撤销调拨给对重闲置内存块的物理存储器。
WINBASEAPI
BOOL
WINAPI
HeapLock(
_In_ HANDLE hHeap
);
WINBASEAPI
BOOL
WINAPI
HeapUnlock(
_In_ HANDLE hHeap
);
用于线程同步。当某个线程要访问堆的链表结构时(例如申请分配内存)其他任何线程试图访问堆都会被挂起。
WINBASEAPI
BOOL
WINAPI
HeapWalk(
_In_ HANDLE hHeap,
_Inout_ LPPROCESS_HEAP_ENTRY lpEntry
);
用于调试,运行遍历堆的内容
typedef struct _PROCESS_HEAP_ENTRY {
PVOID lpData;
DWORD cbData;
BYTE cbOverhead;
BYTE iRegionIndex;
WORD wFlags;
union {
struct {
HANDLE hMem;
DWORD dwReserved[ 3 ];
} Block;
struct {
DWORD dwCommittedSize;
DWORD dwUnCommittedSize;
LPVOID lpFirstBlock;
LPVOID lpLastBlock;
} Region;
} DUMMYUNIONNAME;
} PROCESS_HEAP_ENTRY, *LPPROCESS_HEAP_ENTRY, *PPROCESS_HEAP_ENTRY;
一个遍历当前进程所有Heap和内存块的例子
void printHeapBlock(HANDLE hHeap) {
if (hHeap == NULL)
return;
PROCESS_HEAP_ENTRY pEntry = { 0 };
pEntry.wFlags = PROCESS_HEAP_REGION; // query the first virtual address used by the region.
unsigned int nCount = 0;
printf("Print the blocks of heaps: %p\n", hHeap);
HeapLock(hHeap); // lock the heap.
while (HeapWalk(hHeap, &pEntry)) {
printf("Block[%d]\t%p\tsize:%d bytes\n",
nCount, pEntry.lpData, pEntry.cbData);
nCount++;
}
HeapUnlock(hHeap);
printf("Total %d blocks.\n", nCount);
}
int _tmain(int argc, TCHAR* argv[], TCHAR * env[])
{
HANDLE hHeaps[25]; // assume the number of heaps is not greater than 25.
DWORD dwHeaps = GetProcessHeaps(25, hHeaps);
for (DWORD id = 0; id < dwHeaps; id++) {
printHeapBlock(hHeaps[id]);
}
return 0;
}