谈一谈Windows中的堆

2021-07-28

如果在Windows中编程应该了解一些Windows的内存管理,而堆(Heap)也属于内存管理的一部分。这篇文章对你理解Windows内存分配的基本原理和调试堆内存问题或许会有所帮助。

Windows Heap概述

下图参考<

  1. 进程默认堆。每个进程启动的时候系统会创建一个默认堆。比如LocalAlloc或者GlobalAlloc也是从进程默认堆上分配内存。你也可以使用GetProcessHeap获取进程默认堆的句柄,然后根据用这个句柄去调用HeapAlloc达到在系统默认堆上分配内存的效果。
  2. C++编程中常用的是malloc和new去申请内存,这些由CRT库提供方法。而根据查看在VS2010之前(包含),CRT库会使用HeapCreate去创建一个堆,供CRT库自己使用。在VS2015以后CRT库的实现,并不会再去创建一个单独的堆,而使用进程默认堆。 (VS2013的CRT源码我并未查看,有兴趣的可以看看VS2013默认的CRT库采用的是进程默认堆还是新建的堆)。
  3. 自建堆。这个泛指程序通过HeapCreate去创建的堆,然后利用HeapAlloc等API去操作堆,比如申请空间。

那么堆管理器是通过调用虚拟管理器的一些方法进行堆管理的实现,比如VirtualAlloc之类的函数。同样应用程序也可以直接使用VirtualAlloc之类的函数对内存进行使用。

说到这里不免有些生涩,我们就写一个示例代码来看看一个进程的堆情况。

  1. #include <windows.h> 
  2. #include <iostream> 
  3. #include <intsafe.h> 
  4.  
  5. using namespace std; 
  6. const char* GetHeapTypeString(HANDLE pHandle) 
  7.   ULONG ulHeapInfo; 
  8.   HeapQueryInformation(pHandle, 
  9.     HeapCompatibilityInformation, 
  10.     &ulHeapInfo, 
  11.     sizeof(ulHeapInfo), 
  12.     NULL); 
  13.   switch (ulHeapInfo) 
  14.   { 
  15.   case 0: 
  16.     return "Standard"
  17.   case 1: 
  18.     return "Look Aside List"
  19.   case 2: 
  20.     return "Low Fragmentation"
  21.   } 
  22.   return "Unknow type"
  23.  
  24. void PrintAllHeaps() 
  25.  
  26.   DWORD dwNumHeap = GetProcessHeaps(0, NULL); 
  27.   if (dwNumHeap == 0) 
  28.   { 
  29.     cout << "No Heap!" << endl; 
  30.     return
  31.   } 
  32.  
  33.   PHANDLE pHeaps; 
  34.   SIZE_T  uBytes; 
  35.   HRESULT Result = SIZETMult(dwNumHeap, sizeof(*pHeaps), &uBytes); 
  36.   if (Result != S_OK) { 
  37.     return
  38.   } 
  39.  
  40.   pHeaps = (PHANDLE)malloc(uBytes); 
  41.   dwNumHeap = GetProcessHeaps(dwNumHeap, pHeaps); 
  42.   cout << "Process has heaps: " << dwNumHeap << endl; 
  43.   for (int i = 0; i < dwNumHeap; ++i) 
  44.   { 
  45.     cout << "Heap Address: " << pHeaps[i] 
  46.       << ", Heap Type: " << GetHeapTypeString(pHeaps[i]) << endl; 
  47.   } 
  48.  
  49.   return
  50.  
  51. int main() 
  52.   cout << "========================" << endl; 
  53.   PrintAllHeaps(); 
  54.   cout << "========================" << endl; 
  55.  
  56.   HANDLE hDefaultHeap = GetProcessHeap(); 
  57.   cout << "Default Heap: " << hDefaultHeap 
  58.     << ", Heap Type: " << GetHeapTypeString(hDefaultHeap) << endl; 
  59.  
  60.   return 0; 

这是一个在Win10上运行的64位程序输出的结果: 这个进程我们并没有在main中显示的创建Heap,我们都知道进程在启动的时候初始化会创建相关的资源,其中也包含了堆。这个进程共创建了四个堆。可以看出来第一个堆就是进程的默认堆,并且是采用的 Low Fragmentation的分配策略的堆。

堆的内存分配策略

堆主要有前端分配器和后端分配器,我所理解的前端分配器就是类似于缓存一样,便于快速的查询所需要的内存块,当前端分配器搞不定的时候,就交给后端分配器。

前端分配器主要分为, 而Windows Vista之后进程默认堆均采用低碎片前端分配器。

  • 旁视列表 (Look Aside List)
  • 低碎片 (Low Fragmentation)

以下的场景均采用32位的程序进行的描述。

前端分配器之旁视列表

旁视列表 (Look Aside List, LAL)是一种老的前端分配器,在Windows XP中使用。

这是一个连续的数组大小为128,每个元素对应一个链表,因为其存储的是整个Heap块的大小,那就包含了用户申请的大小+堆块元数据,而这里元数据大小为8字节, 而最小分配粒度为8字节(32位程序),那么最小的堆块的大小则为16个字节。从数据1~127,每个链表锁存储的堆块大小按照8字节粒度增加。

那么当用户申请一个比如10字节大小的的内存,则在LAL中查找的堆块大小为18字节=10字节+元数据8字节,则在表中找到的刚好匹配的堆块大小为24字节的节点,并将其从链表中删除。

而当用户释放内存的时候,也会优先查看前端处理器是否处理,如果处理则将内存插入到相应的链表中。

前端分配器之低碎片

先说说内存碎片我这里简要概述下: 如下图所示假设一段大的连续的内存被分割为若干个8字节的内存块,然后这个时候释放了图中绿色部分的内存块,那么此时总共空出了40字节的内存,但想去申请一个16字节的内存块,却无法申请到一个连续的16字节内存块,从而分配内存失败,这就是内存碎片。

所谓的低碎片前端分配器,是将LAL类似的数组中的粒度重新进行了划分:

数据Index 堆块递增粒度 堆块字节范围
0~31 8 8~256
32~47 16 272~512
112-127 512 8704~16384
 

可以看到同样的数组的大小,将其按照不同的粒度划分,相比较LAL分配的大小粒度逐步增大,到了最后的112-127区间粒度已经增大到了512字节,最大支持的16384。粒度更大的分配有利于缓解内存碎片,提高内存的使用效率。Windows Vista之后进程默认堆均采用低碎片前端分配器。

后端分配器

其实讲到前面这部分可能还有一些人云里雾里。那么我们的内存到底是怎么划分出来的呢?这就是后端分配器要做的事情了。看看后端分配器是如何管理这些内存的。

先说说堆在内存中的展现形式,一个堆主要由若干个Segment(段)组成,每个Segment都是一段连续的空间,然后用双向链表串起来。而一般情况下,一开始只有一个Segment,然后在这个Segment上申请空间,叫做Heap Entry(堆块)。但是这个Segment可能会被用完,那就新开辟一个Segment,而且一般新的Segement大小是原先的2倍,如果内存不足则不断的将申请空间减半。这里有个要注意的就是当划分了一个新的Segment后比如其空间为1GBytes,那么其真实的使用的物理内存肯定不会是1GBytes,因为此时内存还没有被应用程序申请,这个时候实际上这个Segment只是Reserve了这段虚拟地址空间,而当真正应用程序申请内存的时候,才会一小部分一小部分的Commit,这个时候才会用到真正的物理存储空间。

而应用程序申请的内存在Segment上叫做Entry(块),他们是连续的,可以看到一个块一般具有:

  • 前置的元数据: 这里主要存储有当前块的大小,前一个块的大小,当前块的状态等。
  • 用户数据区: 这段内存才是用户申请并且使用的内存。当然这块数据可能比你申请的内存要大一些,因为32位下面最小的分配粒度是8字节。这也是为什么有时候程序有时候溢出了几个字符,好像也没有导致程序异常或者崩溃的原因。
  • 后置的元数据: 这个一般用于调试所用。一般发布的时候不会占用这块空间。

那么哪些块是可以直接使用的呢?这就涉及到这些块元数据中的状态,可以表明这个块是否被占用,如果是空闲状态则可以使用。

后端分配器,不会傻傻的去遍历所有的块的状态来决定是否可以分配吧?这个时候就用到了后端分配器的策略。

这个表有点类似于LAL, 只是注意看下这个index为0的多了一个list,从小到大排列,可变大小的从大于1016字节的小于524272字节的将在这个链表里面存储。超过524272字节将直接通过VirtualAlloc之类的API直接获取内存。

假设此时前端堆管理器需要寻找一个32字节的堆块, 后端管理器将如何操作?

这个时候请求到了后端分配器,后端分配器假设也没有在这个表中查找到32字节的空闲块,那么将先查找64字节的空闲块,如果找到,则将其从列表中移除,然后将其分割为两个16字节的块, 一个设置为占用状态返回给应用程序,一个设置为空闲状态插入响应的链表中。

那如果还没有找到呢?那么这个时候堆管理器会从Segment中提交(Commit)更多的内存去使用,创建新的块, 如果当前Segment空间也不够了,那就创建新的Segement

有细心的同学可能说,那前端分配器和后端分配器差不多吗,这里面有个很重要的就是,前端分配器链表中的块是属于占用状态的, 而后端分配器链表中的块是属于空闲状态的。

假设释放内存,该如何操作?

首先要看前端分配器是否处理这个释放的块,比如加入到相应的链表中去,如果不处理,那么后端分配器将会查看相邻的块是否也是空闲的,如果是空闲状态,将会采用块合并成一个大的块,并对相应的后端分配器链表进行操作。

当然了当你释放的内存足够多的时候,其实堆管理器也不会长期霸占着物理存储器的空间,也会在适当的情况下调用Decommit操作来减少物理存储器的使用。

Windbg查看进程中的堆

进程堆信息查看

进程堆的信息是放在PEB(进程环境块)中,可以通过查看PEB相关的信息, 可以看到当前进程包含有3个堆,并且堆的数组地址为0x77756660

  1. 0:000> dt _PEB @$peb 
  2.    ...... 
  3.    +0x088 NumberOfHeaps    : 3 
  4.    ...... 
  5.    +0x090 ProcessHeaps     : 0x77756660  -> 0x00fa0000 Void 
  6.  
  7.    ...... 

然后我们查看对应的三个堆的地址,分别为0xfa0000, 0x14b0000和0x2e10000, 而第一个一般为进程的默认堆00fa0000。

  1. 0:006> dd 0x77756660 
  2. 77756660  00fa0000 014b0000 02e10000 00000000 
  3. 77756670  00000000 00000000 00000000 00000000 
  4. 77756680  00000000 00000000 00000000 00000000 
  5. 77756690  00000000 00000000 00000000 00000000 
  6. 777566a0  00000000 00000000 00000000 00000000 
  7. 777566b0  00000000 00000000 00000000 00000000 
  8. 777566c0  ffffffff ffffffff 00000000 00000000 
  9. 777566d0  00000000 020007d0 00000000 00000000 

其实上述步骤Windbg提供了一个方法可以直接查看概要信息了, 可以看到系统默认堆00fa0000为LFH堆,并且已经Reserve了空间为1128K, Commit的内存为552K。

  1. 0:000> !heap -s 
  2. ...... 
  3. LFH Key                   : 0x8302caa1 
  4. Termination on corruption : ENABLED 
  5.   Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast  
  6.                     (k)     (k)    (k)     (k) length      blocks cont. heap  
  7. ----------------------------------------------------------------------------- 
  8. 00fa0000 00000002    1128    552   1020    178    21     1    1      0   LFH 
  9. 014b0000 00001002      60     12     60      1     2     1    0      0       
  10. 02e10000 00001002    1188     92   1080      4     4     2    0      0   LFH 
  11. ----------------------------------------------------------------------------- 

可以通过dt _HEAP 00fa0000命令去查看进程默认堆的信息,也可以通过Windbg直接提供的命令去查看, 可以看到其分配空间的最小粒度(Granularity)为8字节。并且只有一个Segment.

  1. 0:006> !heap -a 00fa0000 
  2. Index   Address  Name      Debugging options enabled 
  3.   1:   00fa0000  
  4.     Segment at 00fa0000 to 0109f000 (00089000 bytes committed
  5.     Flags:                00000002 
  6.     ForceFlags:           00000000 
  7.     Granularity:          8 bytes 
  8.     Segment Reserve:      00100000 
  9.     Segment Commit:       00002000 
  10.     DeCommit Block Thres: 00000800 
  11.     DeCommit Total Thres: 00002000 
  12.     Total Free Size:      0000597f 
  13.     Max. Allocation Size: 7ffdefff 
  14.     Lock Variable at:     00fa0248 
  15.     Next TagIndex:        0000 
  16.     Maximum TagIndex:     0000 
  17.     Tag Entries:          00000000 
  18.     PsuedoTag Entries:    00000000 
  19.     Virtual Alloc List:   00fa009c 
  20.         03321000: 00100000 [commited 101000, unused 1000] - busy (b), tail fill 
  21.     Uncommitted ranges:   00fa008c 
  22.             01029000: 00076000  (483328 bytes) 
  23.     FreeList[ 00 ] at 00fa00c0: 00ffcf40 . 00ff3290   
  24.         00ff3288: 00208 . 00010 [100] - free 
  25.         00fb1370: 00060 . 00010 [100] - free 
  26.         00fb10a0: 00020 . 00010 [100] - free 
  27.         00fa6c40: 00088 . 00010 [100] - free 
  28.         00fa8e98: 00010 . 00010 [100] - free 
  29.         00fafa78: 000d0 . 00018 [100] - free 
  30.         00faea20: 00138 . 00018 [100] - free 
  31.         00fafc38: 00030 . 00020 [100] - free 
  32.         00ff4570: 00128 . 00028 [100] - free 
  33.         00faeeb8: 00058 . 00028 [100] - free 
  34.         00faf0c8: 00060 . 00028 [100] - free 
  35.         00fad980: 00050 . 00028 [100] - free 
  36.         00fb83f0: 00050 . 00040 [100] - free 
  37.         00faed78: 00030 . 00080 [100] - free 
  38.         00feebd8: 000e8 . 00080 [100] - free 
  39.         00faeb80: 00050 . 000d0 [100] - free 
  40.         00ff0398: 00148 . 000d8 [100] - free 
  41.         00fafed0: 000b0 . 000f0 [100] - free 
  42.         00fb8130: 00210 . 00270 [100] - free 
  43.         00fef460: 00808 . 003c8 [100] - free 
  44.         00ffcf38: 003c8 . 2c0a8 [100] - free 
  45.  
  46.     Segment00 at 00fa0000: 
  47.         Flags:           00000000 
  48.         Base:            00fa0000 
  49.         First Entry:     00fa0498 
  50.         Last Entry:      0109f000 
  51.         Total Pages:     000000ff 
  52.         Total UnCommit:  00000076 
  53.         Largest UnCommit:00000000 
  54.         UnCommitted Ranges: (1) 
  55.  
  56.     Heap entries for Segment00 in Heap 00fa0000 
  57.          address: psize . size  flags   state (requested size
  58.         00fa0000: 00000 . 00498 [101] - busy (497) 
  59.         00fa0498: 00498 . 00108 [101] - busy (100) 
  60.         00fa05a0: 00108 . 000d8 [101] - busy (d0) 
  61.  
  62.         ...... 
  63.         01029000:      00076000      - uncommitted bytes. 

查看Segment

一般来说我们通过上述的命令已经可以基本查看到Segment在一个堆中的信息了。如果要针对一个Segment进行查看可以用如下方式:

  1. 0:006> dt _HEAP_SEGMENT 00fa0000 
  2. ntdll!_HEAP_SEGMENT 
  3.    +0x000 Entry            : _HEAP_ENTRY 
  4.    +0x008 SegmentSignature : 0xffeeffee 
  5.    +0x00c SegmentFlags     : 2 
  6.    +0x010 SegmentListEntry : _LIST_ENTRY [ 0xfa00a4 - 0xfa00a4 ] 
  7.    +0x018 Heap             : 0x00fa0000 _HEAP 
  8.    +0x01c BaseAddress      : 0x00fa0000 Void 
  9.    +0x020 NumberOfPages    : 0xff 
  10.    +0x024 FirstEntry       : 0x00fa0498 _HEAP_ENTRY 
  11.    +0x028 LastValidEntry   : 0x0109f000 _HEAP_ENTRY 
  12.    +0x02c NumberOfUnCommittedPages : 0x76 
  13.    +0x030 NumberOfUnCommittedRanges : 1 
  14.    +0x034 SegmentAllocatorBackTraceIndex : 0 
  15.    +0x036 Reserved         : 0 
  16.    +0x038 UCRSegmentList   : _LIST_ENTRY [ 0x1028ff0 - 0x1028ff0 ] 

查看申请的内存地址

其实在调试过程中一般最关注的是变量的地址关联的内容信息。比如说我写了个程序其申请的内存变量地址为0x00fb5440, 申请的大小为5字节。

首先可以通过如下命令查找到地址所在的位置为堆:

  1. 0:000> !address 0x00fb5440 
  2.  
  3. Building memory map: 00000000 
  4. Mapping file section regions... 
  5. Mapping module regions... 
  6. Mapping PEB regions... 
  7. Mapping TEB and stack regions... 
  8. Mapping heap regions... 
  9. Mapping page heap regions... 
  10. Mapping other regions... 
  11. Mapping stack trace database regions... 
  12. Mapping activation context regions... 
  13.  
  14. Usage:                  Heap 
  15. Base Address:           00fa0000 
  16. End Address:            01029000 
  17. Region Size:            00089000 ( 548.000 kB) 
  18. State:                  00001000          MEM_COMMIT 
  19. Protect:                00000004          PAGE_READWRITE 
  20. Type:                   00020000          MEM_PRIVATE 
  21. Allocation Base:        00fa0000 
  22. Allocation Protect:     00000004          PAGE_READWRITE 
  23. More info:              heap owning the address: !heap 0xfa0000 
  24. More info:              heap segment 
  25. More info:              heap entry containing the address: !heap -x 0xfb5440 

然后可以通过如下命令查看当前申请内存的详细堆块信息, 其处于被占用状态(busy)。可以看到其堆块的大小为0x10, 我们实际申请的内存为5字节,那么0x10(Size) - 0xb (Unused) = 5, 可以看出来Unused是包含了_HEAP_ENTRY块元数据的大小的。而我们实际用户可用的内存是8字节 (最小分配粒度),比我们申请的5字节多了三个字节,这也是为什么程序有时候溢出了几个字符,并没有导致程序崩溃或者异常的原因。

  1. 0:000> !heap -x 0xfb5440 
  2. Entry     User      Heap      Segment       Size  PrevSize  Unused    Flags 
  3. ----------------------------------------------------------------------------- 
  4. 00fb5438  00fb5440  00fa0000  00fad348        10      -            b  LFH;busy 

那么我们也可以直接查看Entry的结构:

  1. 0:000> dt _HEAP_ENTRY 00fb5438 
  2. ntdll!_HEAP_ENTRY 
  3.    +0x000 UnpackedEntry    : _HEAP_UNPACKED_ENTRY 
  4.    +0x000 Size             : 0xa026 
  5.    +0x002 Flags            : 0xdc '' 
  6.    +0x003 SmallTagIndex    : 0x83 '' 
  7.    +0x000 SubSegmentCode   : 0x83dca026 
  8.    +0x004 PreviousSize     : 0x1b00 
  9.    +0x006 SegmentOffset    : 0 '' 
  10.    +0x006 LFHFlags         : 0 '' 
  11.    +0x007 UnusedBytes      : 0x8b '' 
  12.    +0x000 ExtendedEntry    : _HEAP_EXTENDED_ENTRY 
  13.    +0x000 FunctionIndex    : 0xa026 
  14.    +0x002 ContextValue     : 0x83dc 
  15.    +0x000 InterceptorValue : 0x83dca026 
  16.    +0x004 UnusedBytesLength : 0x1b00 
  17.    +0x006 EntryOffset      : 0 '' 
  18.    +0x007 ExtendedBlockSignature : 0x8b '' 
  19.    +0x000 Code1            : 0x83dca026 
  20.    +0x004 Code2            : 0x1b00 
  21.    +0x006 Code3            : 0 '' 
  22.    +0x007 Code4            : 0x8b '' 
  23.    +0x004 Code234          : 0x8b001b00 
  24.    +0x000 AgregateCode     : 0x8b001b00`83dca026 

如果细心的同学可以能会发现以下两个问题:

  1. 结构中Size的值是0xa026和之前命令中看到的大小0x10不一样,这个是因为Windows对这些元数据做了编码,需要用堆中的一个编码数据做异或操作才能得到真实的值。具体方法笔者试过,在这里不在赘述,可以在参考文章中获取方法。
  2. Size是2字节描述,那么最大可以描述的大小应该为0xffff,但是之前不是说最大的块可以是0x7FFF0 (524272字节), 应该不够存储啊?这个也和第一个问题有关联,在通过上述方法计算出的Size之后还需要乘以8, 才是真正的数据大小。

Windows 自建堆的使用建议

在<

保护组件

先看看书中原话:

假如你的应用程序需要保护两个组件,一个是节点结构的链接表,一个是 B R A N C H结构的二进制树。你有两个源代码文件,一个是 L n k L s t . c p p,它包含负责处理N O D E链接表的各个函数,另一个文件是 B i n Tr e e . c p p,它包含负责处理分支的二进制树的各个函数。

现在假设链接表代码中有一个错误,它使节点 1后面的8个字节不

小心被改写了,从而导致分支 3中的数据被破坏。当B i n Tr e e . c p p文件中的代码后来试图遍历二进制树时,它将无法进行这项操作,因为它的内存已经被破坏。当然,这使你认为二进制树代码中存在一个错误,而实际上错误是在链接表代码中。由于不同类型的对象混合放在单个堆栈中,因此跟踪和确定错误将变得非常困难。

我个人认为在一个应用的工程中,也许不需要做到上述那么精细的划分。但是你想一想,在一个大型工程中,会混合多个模块。比如你是做产品的,那么产品会集成其他部门甚至是外部第三方的组件,那么这些组件同时在同一个进程,使用同一个堆的时候,那么难免会出现,A模块的内存溢出问题,导致了B模块的数据处理异常,从而让你追踪问题异常复杂,更坑的是,很可能让B模块的团队背锅了。而这些是切实存在的。 这里的建议更适合于让一些关键模块使用自己的堆,从而降低自己内存使用不当,覆盖了其他组件使用的内存,从而导致异常,让问题的追踪可以集中在出错的模块中。当然这也不是绝对的,因为进程的组件都在同一个地址空间内,内存破坏也存在一种跳跃式内存访问破坏,但是大多数时候内存溢出是连续的上溢较多,这样做确实可以提高这种问题追踪的效率。

更有效的内存管理

这个主要强调是,将同种类型大小的对象放在一个堆中,尽量避免不同大小内存对象掺杂在一起导致的内存碎片问题,从而带来的堆管理效率下降。同一种对象,则可以避免内存碎片问题。当然了这些只是提供了一种思想,至于你的工程是否有必要采用这样的做法,由工程师自己来做决定。

进行本地访问

先来看看原文的描述:

每当系统必须在 R A M与系统的页文件之间进行 R A M页面的交换时,系统的运行性能就会受到很大的影响。如果经常访问局限于一个小范围地址的内存,那么系统就不太可能需要在 R A M与磁盘之间进行页面的交换。

所以,在设计应用程序的时候,如果有些数据将被同时访问,那么最好把它们分配在互相靠近的位置上。让我们回到链接表和二进制树的例子上来,遍历链接表与遍历二进制树之间并无什么关系。如果将所有的节点放在一起(放在一个堆栈中),就可以使这些节点位于相邻的页面上。实际上,若干个节点很可能恰好放入单个物理内存页面上。遍历链接表将不需要 C P U为了访问每个节点而引用若干不同的内存页面。

这个思想其实就是一种Cache思想,RAM与磁盘上的page.sys存储器(磁盘上的虚拟内存)进行页交换会带来一些时间成本。举个极限的例子,你的RAM只有一个页,你有两个对象A和B,A存放在Page1上,而B存放在Page2上,当你访问A对象的时候,必然要把Page1的内容加载到RAM中,那么这个时候B对象所在Page2肯定就在page.sys中,当你又访问B对象的时候,这个时候就得把Page2从page.sys中加载到RAM中替换掉Page1.

理解了页切换带来的性能开销后,其实这一段的思想就是将最可能连续访问的对象放在一个堆中,那么他们在一个页面的可能性也更大,提高了效率。

减少线程同步的开销

这一个很好理解,一般情况下创建的自建堆是支持多线程的,那么多线程的内存分配必然会带来同步的时间消耗,但是对于有些工程来说,只有一个线程,那么对于这一个线程的程序,在调用HeapCreate的时候设置HEAP_NO_SERIALIZE, 则这个堆只支持单线程,从而提高内存申请的效率。

迅速释放堆栈

这种思想第一提高了内存释放的效率,第二是尽可能的降低了内存泄露。记得之前看过一篇文章介绍过Arena感觉比较类似,在一个生命周期内的内存是从Arena申请,然后这个声明周期结束后,不是直接释放各个对象,而是直接销毁这个Arena,提高了释放效率,并且降低了内存泄露的可能。那么使用自建堆的原理和Arena是类似的,比如在一个任务处理之前创建一个堆,在任务处理过程中所申请的内存在这个堆上申请,然后释放的时候,直接销毁这个堆即可。

那对于对象的申请,C++中可以重载new和delete等操作符,来实现自定义的内存分配,并且可以将这个先封装成一个基类,在这个过程中需要创建的对象均继承于这个基类,复用new和delete。

总结和参考

我本以为这些是已经掌握的知识,但是写文章的时间也超过了我预想的时间,在实践中也也发现了一些自己曾经错误的理解。如果文中还有不当的地方,也希望读者给与指正。

参考

《Windows核心编程》

《Windows高级调试》

Windows Heap Chunk Header Parsing and Size Calculation: https://stackoverflow.com/questions/28483473/windows-heap-chunk-header-parsing-and-size-calculation

Understanding the Low Fragmentation Heap: http://www.illmatics.com/Understanding_the_LFH.pdf

WINDOWS 10SEGMENT HEAP INTERNALS: https://www.blackhat.com/docs/us-16/materials/us-16-Yason-Windows-10-Segment-Heap-Internals-wp.pdf