談一談Windows中的堆
本文轉(zhuǎn)載自微信公眾號(hào)「一個(gè)程序員的修煉之路」,作者河邊一枝柳。轉(zhuǎn)載本文請(qǐng)聯(lián)系一個(gè)程序員的修煉之路公眾號(hào)。
如果在Windows中編程應(yīng)該了解一些Windows的內(nèi)存管理,而堆(Heap)也屬于內(nèi)存管理的一部分。這篇文章對(duì)你理解Windows內(nèi)存分配的基本原理和調(diào)試堆內(nèi)存問(wèn)題或許會(huì)有所幫助。
Windows Heap概述
下圖參考<<Windows高級(jí)調(diào)試>>所畫(huà),并做了一些小小的修改。可以看出來(lái)程序中對(duì)堆的直接操作主要有三種:
- 進(jìn)程默認(rèn)堆。每個(gè)進(jìn)程啟動(dòng)的時(shí)候系統(tǒng)會(huì)創(chuàng)建一個(gè)默認(rèn)堆。比如LocalAlloc或者GlobalAlloc也是從進(jìn)程默認(rèn)堆上分配內(nèi)存。你也可以使用GetProcessHeap獲取進(jìn)程默認(rèn)堆的句柄,然后根據(jù)用這個(gè)句柄去調(diào)用HeapAlloc達(dá)到在系統(tǒng)默認(rèn)堆上分配內(nèi)存的效果。
- C++編程中常用的是malloc和new去申請(qǐng)內(nèi)存,這些由CRT庫(kù)提供方法。而根據(jù)查看在VS2010之前(包含),CRT庫(kù)會(huì)使用HeapCreate去創(chuàng)建一個(gè)堆,供CRT庫(kù)自己使用。在VS2015以后CRT庫(kù)的實(shí)現(xiàn),并不會(huì)再去創(chuàng)建一個(gè)單獨(dú)的堆,而使用進(jìn)程默認(rèn)堆。 (VS2013的CRT源碼我并未查看,有興趣的可以看看VS2013默認(rèn)的CRT庫(kù)采用的是進(jìn)程默認(rèn)堆還是新建的堆)。
- 自建堆。這個(gè)泛指程序通過(guò)HeapCreate去創(chuàng)建的堆,然后利用HeapAlloc等API去操作堆,比如申請(qǐng)空間。
那么堆管理器是通過(guò)調(diào)用虛擬管理器的一些方法進(jìn)行堆管理的實(shí)現(xiàn),比如VirtualAlloc之類(lèi)的函數(shù)。同樣應(yīng)用程序也可以直接使用VirtualAlloc之類(lèi)的函數(shù)對(duì)內(nèi)存進(jìn)行使用。
說(shuō)到這里不免有些生澀,我們就寫(xiě)一個(gè)示例代碼來(lái)看看一個(gè)進(jìn)程的堆情況。
- #include <windows.h>
- #include <iostream>
- #include <intsafe.h>
- using namespace std;
- const char* GetHeapTypeString(HANDLE pHandle)
- {
- ULONG ulHeapInfo;
- HeapQueryInformation(pHandle,
- HeapCompatibilityInformation,
- &ulHeapInfo,
- sizeof(ulHeapInfo),
- NULL);
- switch (ulHeapInfo)
- {
- case 0:
- return "Standard";
- case 1:
- return "Look Aside List";
- case 2:
- return "Low Fragmentation";
- }
- return "Unknow type";
- }
- void PrintAllHeaps()
- {
- DWORD dwNumHeap = GetProcessHeaps(0, NULL);
- if (dwNumHeap == 0)
- {
- cout << "No Heap!" << endl;
- return;
- }
- PHANDLE pHeaps;
- SIZE_T uBytes;
- HRESULT Result = SIZETMult(dwNumHeap, sizeof(*pHeaps), &uBytes);
- if (Result != S_OK) {
- return;
- }
- pHeaps = (PHANDLE)malloc(uBytes);
- dwNumHeap = GetProcessHeaps(dwNumHeap, pHeaps);
- cout << "Process has heaps: " << dwNumHeap << endl;
- for (int i = 0; i < dwNumHeap; ++i)
- {
- cout << "Heap Address: " << pHeaps[i]
- << ", Heap Type: " << GetHeapTypeString(pHeaps[i]) << endl;
- }
- return;
- }
- int main()
- {
- cout << "========================" << endl;
- PrintAllHeaps();
- cout << "========================" << endl;
- HANDLE hDefaultHeap = GetProcessHeap();
- cout << "Default Heap: " << hDefaultHeap
- << ", Heap Type: " << GetHeapTypeString(hDefaultHeap) << endl;
- return 0;
- }
這是一個(gè)在Win10上運(yùn)行的64位程序輸出的結(jié)果: 這個(gè)進(jìn)程我們并沒(méi)有在main中顯示的創(chuàng)建Heap,我們都知道進(jìn)程在啟動(dòng)的時(shí)候初始化會(huì)創(chuàng)建相關(guān)的資源,其中也包含了堆。這個(gè)進(jìn)程共創(chuàng)建了四個(gè)堆。可以看出來(lái)第一個(gè)堆就是進(jìn)程的默認(rèn)堆,并且是采用的 Low Fragmentation的分配策略的堆。
堆的內(nèi)存分配策略
堆主要有前端分配器和后端分配器,我所理解的前端分配器就是類(lèi)似于緩存一樣,便于快速的查詢所需要的內(nèi)存塊,當(dāng)前端分配器搞不定的時(shí)候,就交給后端分配器。
前端分配器主要分為, 而Windows Vista之后進(jìn)程默認(rèn)堆均采用低碎片前端分配器。
- 旁視列表 (Look Aside List)
- 低碎片 (Low Fragmentation)
以下的場(chǎng)景均采用32位的程序進(jìn)行的描述。
前端分配器之旁視列表
旁視列表 (Look Aside List, LAL)是一種老的前端分配器,在Windows XP中使用。
這是一個(gè)連續(xù)的數(shù)組大小為128,每個(gè)元素對(duì)應(yīng)一個(gè)鏈表,因?yàn)槠浯鎯?chǔ)的是整個(gè)Heap塊的大小,那就包含了用戶申請(qǐng)的大小+堆塊元數(shù)據(jù),而這里元數(shù)據(jù)大小為8字節(jié), 而最小分配粒度為8字節(jié)(32位程序),那么最小的堆塊的大小則為16個(gè)字節(jié)。從數(shù)據(jù)1~127,每個(gè)鏈表鎖存儲(chǔ)的堆塊大小按照8字節(jié)粒度增加。
那么當(dāng)用戶申請(qǐng)一個(gè)比如10字節(jié)大小的的內(nèi)存,則在LAL中查找的堆塊大小為18字節(jié)=10字節(jié)+元數(shù)據(jù)8字節(jié),則在表中找到的剛好匹配的堆塊大小為24字節(jié)的節(jié)點(diǎn),并將其從鏈表中刪除。
而當(dāng)用戶釋放內(nèi)存的時(shí)候,也會(huì)優(yōu)先查看前端處理器是否處理,如果處理則將內(nèi)存插入到相應(yīng)的鏈表中。
前端分配器之低碎片
先說(shuō)說(shuō)內(nèi)存碎片我這里簡(jiǎn)要概述下: 如下圖所示假設(shè)一段大的連續(xù)的內(nèi)存被分割為若干個(gè)8字節(jié)的內(nèi)存塊,然后這個(gè)時(shí)候釋放了圖中綠色部分的內(nèi)存塊,那么此時(shí)總共空出了40字節(jié)的內(nèi)存,但想去申請(qǐng)一個(gè)16字節(jié)的內(nèi)存塊,卻無(wú)法申請(qǐng)到一個(gè)連續(xù)的16字節(jié)內(nèi)存塊,從而分配內(nèi)存失敗,這就是內(nèi)存碎片。
所謂的低碎片前端分配器,是將LAL類(lèi)似的數(shù)組中的粒度重新進(jìn)行了劃分:
數(shù)據(jù)Index | 堆塊遞增粒度 | 堆塊字節(jié)范圍 |
---|---|---|
0~31 | 8 | 8~256 |
32~47 | 16 | 272~512 |
… | … | … |
112-127 | 512 | 8704~16384 |
可以看到同樣的數(shù)組的大小,將其按照不同的粒度劃分,相比較LAL分配的大小粒度逐步增大,到了最后的112-127區(qū)間粒度已經(jīng)增大到了512字節(jié),最大支持的16384。粒度更大的分配有利于緩解內(nèi)存碎片,提高內(nèi)存的使用效率。Windows Vista之后進(jìn)程默認(rèn)堆均采用低碎片前端分配器。
后端分配器
其實(shí)講到前面這部分可能還有一些人云里霧里。那么我們的內(nèi)存到底是怎么劃分出來(lái)的呢?這就是后端分配器要做的事情了。看看后端分配器是如何管理這些內(nèi)存的。
先說(shuō)說(shuō)堆在內(nèi)存中的展現(xiàn)形式,一個(gè)堆主要由若干個(gè)Segment(段)組成,每個(gè)Segment都是一段連續(xù)的空間,然后用雙向鏈表串起來(lái)。而一般情況下,一開(kāi)始只有一個(gè)Segment,然后在這個(gè)Segment上申請(qǐng)空間,叫做Heap Entry(堆塊)。但是這個(gè)Segment可能會(huì)被用完,那就新開(kāi)辟一個(gè)Segment,而且一般新的Segement大小是原先的2倍,如果內(nèi)存不足則不斷的將申請(qǐng)空間減半。這里有個(gè)要注意的就是當(dāng)劃分了一個(gè)新的Segment后比如其空間為1GBytes,那么其真實(shí)的使用的物理內(nèi)存肯定不會(huì)是1GBytes,因?yàn)榇藭r(shí)內(nèi)存還沒(méi)有被應(yīng)用程序申請(qǐng),這個(gè)時(shí)候?qū)嶋H上這個(gè)Segment只是Reserve了這段虛擬地址空間,而當(dāng)真正應(yīng)用程序申請(qǐng)內(nèi)存的時(shí)候,才會(huì)一小部分一小部分的Commit,這個(gè)時(shí)候才會(huì)用到真正的物理存儲(chǔ)空間。
而應(yīng)用程序申請(qǐng)的內(nèi)存在Segment上叫做Entry(塊),他們是連續(xù)的,可以看到一個(gè)塊一般具有:
- 前置的元數(shù)據(jù): 這里主要存儲(chǔ)有當(dāng)前塊的大小,前一個(gè)塊的大小,當(dāng)前塊的狀態(tài)等。
- 用戶數(shù)據(jù)區(qū): 這段內(nèi)存才是用戶申請(qǐng)并且使用的內(nèi)存。當(dāng)然這塊數(shù)據(jù)可能比你申請(qǐng)的內(nèi)存要大一些,因?yàn)?2位下面最小的分配粒度是8字節(jié)。這也是為什么有時(shí)候程序有時(shí)候溢出了幾個(gè)字符,好像也沒(méi)有導(dǎo)致程序異常或者崩潰的原因。
- 后置的元數(shù)據(jù): 這個(gè)一般用于調(diào)試所用。一般發(fā)布的時(shí)候不會(huì)占用這塊空間。
那么哪些塊是可以直接使用的呢?這就涉及到這些塊元數(shù)據(jù)中的狀態(tài),可以表明這個(gè)塊是否被占用,如果是空閑狀態(tài)則可以使用。
后端分配器,不會(huì)傻傻的去遍歷所有的塊的狀態(tài)來(lái)決定是否可以分配吧?這個(gè)時(shí)候就用到了后端分配器的策略。
這個(gè)表有點(diǎn)類(lèi)似于LAL, 只是注意看下這個(gè)index為0的多了一個(gè)list,從小到大排列,可變大小的從大于1016字節(jié)的小于524272字節(jié)的將在這個(gè)鏈表里面存儲(chǔ)。超過(guò)524272字節(jié)將直接通過(guò)VirtualAlloc之類(lèi)的API直接獲取內(nèi)存。
假設(shè)此時(shí)前端堆管理器需要尋找一個(gè)32字節(jié)的堆塊, 后端管理器將如何操作?
這個(gè)時(shí)候請(qǐng)求到了后端分配器,后端分配器假設(shè)也沒(méi)有在這個(gè)表中查找到32字節(jié)的空閑塊,那么將先查找64字節(jié)的空閑塊,如果找到,則將其從列表中移除,然后將其分割為兩個(gè)16字節(jié)的塊, 一個(gè)設(shè)置為占用狀態(tài)返回給應(yīng)用程序,一個(gè)設(shè)置為空閑狀態(tài)插入響應(yīng)的鏈表中。
那如果還沒(méi)有找到呢?那么這個(gè)時(shí)候堆管理器會(huì)從Segment中提交(Commit)更多的內(nèi)存去使用,創(chuàng)建新的塊, 如果當(dāng)前Segment空間也不夠了,那就創(chuàng)建新的Segement
有細(xì)心的同學(xué)可能說(shuō),那前端分配器和后端分配器差不多嗎,這里面有個(gè)很重要的就是,前端分配器鏈表中的塊是屬于占用狀態(tài)的, 而后端分配器鏈表中的塊是屬于空閑狀態(tài)的。
假設(shè)釋放內(nèi)存,該如何操作?
首先要看前端分配器是否處理這個(gè)釋放的塊,比如加入到相應(yīng)的鏈表中去,如果不處理,那么后端分配器將會(huì)查看相鄰的塊是否也是空閑的,如果是空閑狀態(tài),將會(huì)采用塊合并成一個(gè)大的塊,并對(duì)相應(yīng)的后端分配器鏈表進(jìn)行操作。
當(dāng)然了當(dāng)你釋放的內(nèi)存足夠多的時(shí)候,其實(shí)堆管理器也不會(huì)長(zhǎng)期霸占著物理存儲(chǔ)器的空間,也會(huì)在適當(dāng)?shù)那闆r下調(diào)用Decommit操作來(lái)減少物理存儲(chǔ)器的使用。
Windbg查看進(jìn)程中的堆
進(jìn)程堆信息查看
進(jìn)程堆的信息是放在PEB(進(jìn)程環(huán)境塊)中,可以通過(guò)查看PEB相關(guān)的信息, 可以看到當(dāng)前進(jìn)程包含有3個(gè)堆,并且堆的數(shù)組地址為0x77756660
- 0:000> dt _PEB @$peb
- ......
- +0x088 NumberOfHeaps : 3
- ......
- +0x090 ProcessHeaps : 0x77756660 -> 0x00fa0000 Void
- ......
然后我們查看對(duì)應(yīng)的三個(gè)堆的地址,分別為0xfa0000, 0x14b0000和0x2e10000, 而第一個(gè)一般為進(jìn)程的默認(rèn)堆00fa0000。
- 0:006> dd 0x77756660
- 77756660 00fa0000 014b0000 02e10000 00000000
- 77756670 00000000 00000000 00000000 00000000
- 77756680 00000000 00000000 00000000 00000000
- 77756690 00000000 00000000 00000000 00000000
- 777566a0 00000000 00000000 00000000 00000000
- 777566b0 00000000 00000000 00000000 00000000
- 777566c0 ffffffff ffffffff 00000000 00000000
- 777566d0 00000000 020007d0 00000000 00000000
其實(shí)上述步驟Windbg提供了一個(gè)方法可以直接查看概要信息了, 可以看到系統(tǒng)默認(rèn)堆00fa0000為L(zhǎng)FH堆,并且已經(jīng)Reserve了空間為1128K, Commit的內(nèi)存為552K。
- 0:000> !heap -s
- ......
- LFH Key : 0x8302caa1
- Termination on corruption : ENABLED
- Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
- (k) (k) (k) (k) length blocks cont. heap
- -----------------------------------------------------------------------------
- 00fa0000 00000002 1128 552 1020 178 21 1 1 0 LFH
- 014b0000 00001002 60 12 60 1 2 1 0 0
- 02e10000 00001002 1188 92 1080 4 4 2 0 0 LFH
- -----------------------------------------------------------------------------
可以通過(guò)dt _HEAP 00fa0000命令去查看進(jìn)程默認(rèn)堆的信息,也可以通過(guò)Windbg直接提供的命令去查看, 可以看到其分配空間的最小粒度(Granularity)為8字節(jié)。并且只有一個(gè)Segment.
- 0:006> !heap -a 00fa0000
- Index Address Name Debugging options enabled
- 1: 00fa0000
- Segment at 00fa0000 to 0109f000 (00089000 bytes committed)
- Flags: 00000002
- ForceFlags: 00000000
- Granularity: 8 bytes
- Segment Reserve: 00100000
- Segment Commit: 00002000
- DeCommit Block Thres: 00000800
- DeCommit Total Thres: 00002000
- Total Free Size: 0000597f
- Max. Allocation Size: 7ffdefff
- Lock Variable at: 00fa0248
- Next TagIndex: 0000
- Maximum TagIndex: 0000
- Tag Entries: 00000000
- PsuedoTag Entries: 00000000
- Virtual Alloc List: 00fa009c
- 03321000: 00100000 [commited 101000, unused 1000] - busy (b), tail fill
- Uncommitted ranges: 00fa008c
- 01029000: 00076000 (483328 bytes)
- FreeList[ 00 ] at 00fa00c0: 00ffcf40 . 00ff3290
- 00ff3288: 00208 . 00010 [100] - free
- 00fb1370: 00060 . 00010 [100] - free
- 00fb10a0: 00020 . 00010 [100] - free
- 00fa6c40: 00088 . 00010 [100] - free
- 00fa8e98: 00010 . 00010 [100] - free
- 00fafa78: 000d0 . 00018 [100] - free
- 00faea20: 00138 . 00018 [100] - free
- 00fafc38: 00030 . 00020 [100] - free
- 00ff4570: 00128 . 00028 [100] - free
- 00faeeb8: 00058 . 00028 [100] - free
- 00faf0c8: 00060 . 00028 [100] - free
- 00fad980: 00050 . 00028 [100] - free
- 00fb83f0: 00050 . 00040 [100] - free
- 00faed78: 00030 . 00080 [100] - free
- 00feebd8: 000e8 . 00080 [100] - free
- 00faeb80: 00050 . 000d0 [100] - free
- 00ff0398: 00148 . 000d8 [100] - free
- 00fafed0: 000b0 . 000f0 [100] - free
- 00fb8130: 00210 . 00270 [100] - free
- 00fef460: 00808 . 003c8 [100] - free
- 00ffcf38: 003c8 . 2c0a8 [100] - free
- Segment00 at 00fa0000:
- Flags: 00000000
- Base: 00fa0000
- First Entry: 00fa0498
- Last Entry: 0109f000
- Total Pages: 000000ff
- Total UnCommit: 00000076
- Largest UnCommit:00000000
- UnCommitted Ranges: (1)
- Heap entries for Segment00 in Heap 00fa0000
- address: psize . size flags state (requested size)
- 00fa0000: 00000 . 00498 [101] - busy (497)
- 00fa0498: 00498 . 00108 [101] - busy (100)
- 00fa05a0: 00108 . 000d8 [101] - busy (d0)
- ......
- 01029000: 00076000 - uncommitted bytes.
查看Segment
一般來(lái)說(shuō)我們通過(guò)上述的命令已經(jīng)可以基本查看到Segment在一個(gè)堆中的信息了。如果要針對(duì)一個(gè)Segment進(jìn)行查看可以用如下方式:
- 0:006> dt _HEAP_SEGMENT 00fa0000
- ntdll!_HEAP_SEGMENT
- +0x000 Entry : _HEAP_ENTRY
- +0x008 SegmentSignature : 0xffeeffee
- +0x00c SegmentFlags : 2
- +0x010 SegmentListEntry : _LIST_ENTRY [ 0xfa00a4 - 0xfa00a4 ]
- +0x018 Heap : 0x00fa0000 _HEAP
- +0x01c BaseAddress : 0x00fa0000 Void
- +0x020 NumberOfPages : 0xff
- +0x024 FirstEntry : 0x00fa0498 _HEAP_ENTRY
- +0x028 LastValidEntry : 0x0109f000 _HEAP_ENTRY
- +0x02c NumberOfUnCommittedPages : 0x76
- +0x030 NumberOfUnCommittedRanges : 1
- +0x034 SegmentAllocatorBackTraceIndex : 0
- +0x036 Reserved : 0
- +0x038 UCRSegmentList : _LIST_ENTRY [ 0x1028ff0 - 0x1028ff0 ]
查看申請(qǐng)的內(nèi)存地址
其實(shí)在調(diào)試過(guò)程中一般最關(guān)注的是變量的地址關(guān)聯(lián)的內(nèi)容信息。比如說(shuō)我寫(xiě)了個(gè)程序其申請(qǐng)的內(nèi)存變量地址為0x00fb5440, 申請(qǐng)的大小為5字節(jié)。
首先可以通過(guò)如下命令查找到地址所在的位置為堆:
- 0:000> !address 0x00fb5440
- Building memory map: 00000000
- Mapping file section regions...
- Mapping module regions...
- Mapping PEB regions...
- Mapping TEB and stack regions...
- Mapping heap regions...
- Mapping page heap regions...
- Mapping other regions...
- Mapping stack trace database regions...
- Mapping activation context regions...
- Usage: Heap
- Base Address: 00fa0000
- End Address: 01029000
- Region Size: 00089000 ( 548.000 kB)
- State: 00001000 MEM_COMMIT
- Protect: 00000004 PAGE_READWRITE
- Type: 00020000 MEM_PRIVATE
- Allocation Base: 00fa0000
- Allocation Protect: 00000004 PAGE_READWRITE
- More info: heap owning the address: !heap 0xfa0000
- More info: heap segment
- More info: heap entry containing the address: !heap -x 0xfb5440
然后可以通過(guò)如下命令查看當(dāng)前申請(qǐng)內(nèi)存的詳細(xì)堆塊信息, 其處于被占用狀態(tài)(busy)。可以看到其堆塊的大小為0x10, 我們實(shí)際申請(qǐng)的內(nèi)存為5字節(jié),那么0x10(Size) - 0xb (Unused) = 5, 可以看出來(lái)Unused是包含了_HEAP_ENTRY塊元數(shù)據(jù)的大小的。而我們實(shí)際用戶可用的內(nèi)存是8字節(jié) (最小分配粒度),比我們申請(qǐng)的5字節(jié)多了三個(gè)字節(jié),這也是為什么程序有時(shí)候溢出了幾個(gè)字符,并沒(méi)有導(dǎo)致程序崩潰或者異常的原因。
- 0:000> !heap -x 0xfb5440
- Entry User Heap Segment Size PrevSize Unused Flags
- -----------------------------------------------------------------------------
- 00fb5438 00fb5440 00fa0000 00fad348 10 - b LFH;busy
那么我們也可以直接查看Entry的結(jié)構(gòu):
- 0:000> dt _HEAP_ENTRY 00fb5438
- ntdll!_HEAP_ENTRY
- +0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY
- +0x000 Size : 0xa026
- +0x002 Flags : 0xdc ''
- +0x003 SmallTagIndex : 0x83 ''
- +0x000 SubSegmentCode : 0x83dca026
- +0x004 PreviousSize : 0x1b00
- +0x006 SegmentOffset : 0 ''
- +0x006 LFHFlags : 0 ''
- +0x007 UnusedBytes : 0x8b ''
- +0x000 ExtendedEntry : _HEAP_EXTENDED_ENTRY
- +0x000 FunctionIndex : 0xa026
- +0x002 ContextValue : 0x83dc
- +0x000 InterceptorValue : 0x83dca026
- +0x004 UnusedBytesLength : 0x1b00
- +0x006 EntryOffset : 0 ''
- +0x007 ExtendedBlockSignature : 0x8b ''
- +0x000 Code1 : 0x83dca026
- +0x004 Code2 : 0x1b00
- +0x006 Code3 : 0 ''
- +0x007 Code4 : 0x8b ''
- +0x004 Code234 : 0x8b001b00
- +0x000 AgregateCode : 0x8b001b00`83dca026
如果細(xì)心的同學(xué)可以能會(huì)發(fā)現(xiàn)以下兩個(gè)問(wèn)題:
- 結(jié)構(gòu)中Size的值是0xa026和之前命令中看到的大小0x10不一樣,這個(gè)是因?yàn)閃indows對(duì)這些元數(shù)據(jù)做了編碼,需要用堆中的一個(gè)編碼數(shù)據(jù)做異或操作才能得到真實(shí)的值。具體方法筆者試過(guò),在這里不在贅述,可以在參考文章中獲取方法。
- Size是2字節(jié)描述,那么最大可以描述的大小應(yīng)該為0xffff,但是之前不是說(shuō)最大的塊可以是0x7FFF0 (524272字節(jié)), 應(yīng)該不夠存儲(chǔ)啊?這個(gè)也和第一個(gè)問(wèn)題有關(guān)聯(lián),在通過(guò)上述方法計(jì)算出的Size之后還需要乘以8, 才是真正的數(shù)據(jù)大小。
Windows 自建堆的使用建議
在<
保護(hù)組件
先看看書(shū)中原話:
假如你的應(yīng)用程序需要保護(hù)兩個(gè)組件,一個(gè)是節(jié)點(diǎn)結(jié)構(gòu)的鏈接表,一個(gè)是 B R A N C H結(jié)構(gòu)的二進(jìn)制樹(shù)。你有兩個(gè)源代碼文件,一個(gè)是 L n k L s t . c p p,它包含負(fù)責(zé)處理N O D E鏈接表的各個(gè)函數(shù),另一個(gè)文件是 B i n Tr e e . c p p,它包含負(fù)責(zé)處理分支的二進(jìn)制樹(shù)的各個(gè)函數(shù)。
現(xiàn)在假設(shè)鏈接表代碼中有一個(gè)錯(cuò)誤,它使節(jié)點(diǎn) 1后面的8個(gè)字節(jié)不
小心被改寫(xiě)了,從而導(dǎo)致分支 3中的數(shù)據(jù)被破壞。當(dāng)B i n Tr e e . c p p文件中的代碼后來(lái)試圖遍歷二進(jìn)制樹(shù)時(shí),它將無(wú)法進(jìn)行這項(xiàng)操作,因?yàn)樗膬?nèi)存已經(jīng)被破壞。當(dāng)然,這使你認(rèn)為二進(jìn)制樹(shù)代碼中存在一個(gè)錯(cuò)誤,而實(shí)際上錯(cuò)誤是在鏈接表代碼中。由于不同類(lèi)型的對(duì)象混合放在單個(gè)堆棧中,因此跟蹤和確定錯(cuò)誤將變得非常困難。
我個(gè)人認(rèn)為在一個(gè)應(yīng)用的工程中,也許不需要做到上述那么精細(xì)的劃分。但是你想一想,在一個(gè)大型工程中,會(huì)混合多個(gè)模塊。比如你是做產(chǎn)品的,那么產(chǎn)品會(huì)集成其他部門(mén)甚至是外部第三方的組件,那么這些組件同時(shí)在同一個(gè)進(jìn)程,使用同一個(gè)堆的時(shí)候,那么難免會(huì)出現(xiàn),A模塊的內(nèi)存溢出問(wèn)題,導(dǎo)致了B模塊的數(shù)據(jù)處理異常,從而讓你追蹤問(wèn)題異常復(fù)雜,更坑的是,很可能讓B模塊的團(tuán)隊(duì)背鍋了。而這些是切實(shí)存在的。 這里的建議更適合于讓一些關(guān)鍵模塊使用自己的堆,從而降低自己內(nèi)存使用不當(dāng),覆蓋了其他組件使用的內(nèi)存,從而導(dǎo)致異常,讓問(wèn)題的追蹤可以集中在出錯(cuò)的模塊中。當(dāng)然這也不是絕對(duì)的,因?yàn)檫M(jìn)程的組件都在同一個(gè)地址空間內(nèi),內(nèi)存破壞也存在一種跳躍式內(nèi)存訪問(wèn)破壞,但是大多數(shù)時(shí)候內(nèi)存溢出是連續(xù)的上溢較多,這樣做確實(shí)可以提高這種問(wèn)題追蹤的效率。
更有效的內(nèi)存管理
這個(gè)主要強(qiáng)調(diào)是,將同種類(lèi)型大小的對(duì)象放在一個(gè)堆中,盡量避免不同大小內(nèi)存對(duì)象摻雜在一起導(dǎo)致的內(nèi)存碎片問(wèn)題,從而帶來(lái)的堆管理效率下降。同一種對(duì)象,則可以避免內(nèi)存碎片問(wèn)題。當(dāng)然了這些只是提供了一種思想,至于你的工程是否有必要采用這樣的做法,由工程師自己來(lái)做決定。
進(jìn)行本地訪問(wèn)
先來(lái)看看原文的描述:
每當(dāng)系統(tǒng)必須在 R A M與系統(tǒng)的頁(yè)文件之間進(jìn)行 R A M頁(yè)面的交換時(shí),系統(tǒng)的運(yùn)行性能就會(huì)受到很大的影響。如果經(jīng)常訪問(wèn)局限于一個(gè)小范圍地址的內(nèi)存,那么系統(tǒng)就不太可能需要在 R A M與磁盤(pán)之間進(jìn)行頁(yè)面的交換。
所以,在設(shè)計(jì)應(yīng)用程序的時(shí)候,如果有些數(shù)據(jù)將被同時(shí)訪問(wèn),那么最好把它們分配在互相靠近的位置上。讓我們回到鏈接表和二進(jìn)制樹(shù)的例子上來(lái),遍歷鏈接表與遍歷二進(jìn)制樹(shù)之間并無(wú)什么關(guān)系。如果將所有的節(jié)點(diǎn)放在一起(放在一個(gè)堆棧中),就可以使這些節(jié)點(diǎn)位于相鄰的頁(yè)面上。實(shí)際上,若干個(gè)節(jié)點(diǎn)很可能恰好放入單個(gè)物理內(nèi)存頁(yè)面上。遍歷鏈接表將不需要 C P U為了訪問(wèn)每個(gè)節(jié)點(diǎn)而引用若干不同的內(nèi)存頁(yè)面。
這個(gè)思想其實(shí)就是一種Cache思想,RAM與磁盤(pán)上的page.sys存儲(chǔ)器(磁盤(pán)上的虛擬內(nèi)存)進(jìn)行頁(yè)交換會(huì)帶來(lái)一些時(shí)間成本。舉個(gè)極限的例子,你的RAM只有一個(gè)頁(yè),你有兩個(gè)對(duì)象A和B,A存放在Page1上,而B(niǎo)存放在Page2上,當(dāng)你訪問(wèn)A對(duì)象的時(shí)候,必然要把Page1的內(nèi)容加載到RAM中,那么這個(gè)時(shí)候B對(duì)象所在Page2肯定就在page.sys中,當(dāng)你又訪問(wèn)B對(duì)象的時(shí)候,這個(gè)時(shí)候就得把Page2從page.sys中加載到RAM中替換掉Page1.
理解了頁(yè)切換帶來(lái)的性能開(kāi)銷(xiāo)后,其實(shí)這一段的思想就是將最可能連續(xù)訪問(wèn)的對(duì)象放在一個(gè)堆中,那么他們?cè)谝粋€(gè)頁(yè)面的可能性也更大,提高了效率。
減少線程同步的開(kāi)銷(xiāo)
這一個(gè)很好理解,一般情況下創(chuàng)建的自建堆是支持多線程的,那么多線程的內(nèi)存分配必然會(huì)帶來(lái)同步的時(shí)間消耗,但是對(duì)于有些工程來(lái)說(shuō),只有一個(gè)線程,那么對(duì)于這一個(gè)線程的程序,在調(diào)用HeapCreate的時(shí)候設(shè)置HEAP_NO_SERIALIZE, 則這個(gè)堆只支持單線程,從而提高內(nèi)存申請(qǐng)的效率。
迅速釋放堆棧
這種思想第一提高了內(nèi)存釋放的效率,第二是盡可能的降低了內(nèi)存泄露。記得之前看過(guò)一篇文章介紹過(guò)Arena感覺(jué)比較類(lèi)似,在一個(gè)生命周期內(nèi)的內(nèi)存是從Arena申請(qǐng),然后這個(gè)聲明周期結(jié)束后,不是直接釋放各個(gè)對(duì)象,而是直接銷(xiāo)毀這個(gè)Arena,提高了釋放效率,并且降低了內(nèi)存泄露的可能。那么使用自建堆的原理和Arena是類(lèi)似的,比如在一個(gè)任務(wù)處理之前創(chuàng)建一個(gè)堆,在任務(wù)處理過(guò)程中所申請(qǐng)的內(nèi)存在這個(gè)堆上申請(qǐng),然后釋放的時(shí)候,直接銷(xiāo)毀這個(gè)堆即可。
那對(duì)于對(duì)象的申請(qǐng),C++中可以重載new和delete等操作符,來(lái)實(shí)現(xiàn)自定義的內(nèi)存分配,并且可以將這個(gè)先封裝成一個(gè)基類(lèi),在這個(gè)過(guò)程中需要?jiǎng)?chuàng)建的對(duì)象均繼承于這個(gè)基類(lèi),復(fù)用new和delete。
總結(jié)和參考
我本以為這些是已經(jīng)掌握的知識(shí),但是寫(xiě)文章的時(shí)間也超過(guò)了我預(yù)想的時(shí)間,在實(shí)踐中也也發(fā)現(xiàn)了一些自己曾經(jīng)錯(cuò)誤的理解。如果文中還有不當(dāng)?shù)牡胤剑蚕Mx者給與指正。
參考
《Windows核心編程》
《Windows高級(jí)調(diào)試》
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