全面介绍Windows内存管理机制及C++内存分配实例(五):堆
本文背景:
在編程中,很多Windows或C++的內存函數不知道有什么區別,更別談有效使用;根本的原因是,沒有清楚的理解操作系統的內存管理機制,本文企圖通過簡單的總結描述,結合實例來闡明這個機制。
本文目的:
對Windows內存管理機制了解清楚,有效的利用C++內存函數管理和使用內存。
本文內容:
本文一共有六節,由于篇幅較多,故按節發表。其他章節請看本人博客的Windows內存管理及C++內存分配實例(一)(二)(三)(四)和(六)。
?
?
5.??????內存管理機制--堆?(Heap)
·????????使用場合
堆是進程創建時在進程空間建立的區域,由堆管理器來管理。一個進程可以有很多個堆。進程有一個默認堆為1M,可以動態的擴大。
當程序需要管理很多小對象時,適合用堆;當需要的空間大于1M時,最好用虛擬內存來管理。
堆的優點是,有堆管理器來替它管理,不需管理具體的事情如頁面邊界
????????和分配粒度等問題,你可以從調用函數看的出來,比VirtualAlloc的參數少了
????????不少。
?????????堆的缺點是分配和釋放的速度比前幾種機制要慢,所以最好不要超過
?????????1M;不像虛擬內存那樣隨時提交和釋放,因為它是由堆管理器決定的。如果
?????????用堆分配1G的空間,需要1分種,而用虛擬內存,則感覺不到任何延遲。????
·????????默認堆
進程默認堆是供所有線程使用的,每當線程需要從堆中分配釋放內存區時,系
?統會同步堆,所以訪問速度較慢。
它的默認大小是1M,同樣的,你可以通過以下鏈接命令改變其大小:
#pragma comment(linker,"/HEAP:102400000,1024000")
第一個值是堆的保留空間,第二個值是堆開始時提交的物理內存大小。本文將堆改變為100M。
當你在程序中擴大了堆提交的物理內存時,進程運行時,物理內存將減少擴大的數量。但是,默認堆總是可以擴大的,不能限制它的最大值。
當你在程序中擴大了堆保留的空間時,進程運行時,可用進程空間將會減少擴大的數量。
每次你用New操作符分配內存時,進程空間會相應的減少,物理內存也會相應的減少。
?
一個重要的提示,本文經過測試,如果你需要的內存塊大部分都超過512K,那么,建堆時給它的初始大小不應該很大,因為,如果你所需內存塊大于512K的話,它不是從堆中分配的,也就是說不用堆中默認的空間,但其仍然屬于堆管理。
?
默認堆的一個用處是系統函數需要利用它運行。比如,Windows2000的字符集是UNICODE的,如果調用ANSI版本的函數,系統需要利用堆來從ANSI到UNICODE的轉換,調用UNICODE版本的函數。
·????????自建堆
ü??????使用場合
保護數據結構:
將不同的數據結構存在不同的堆中,可以防止不同的結構之間由于指針誤操作而破壞了它們。
消除內存碎片:
將大小不同的結構保存在一個堆中,會導致碎片的產生,比如釋放一個小結構時,大結構也不能利用它。
獨享堆的快速:
如果用默認堆的話,線程之間是同步訪問,速度慢;如果創建獨享堆,則系統可以不需同步,比較快。
第二個快速體現在釋放的快速,默認堆中,你只能釋放某個內存塊,而不能釋放整個堆;而獨享堆可以一次釋放堆,也就是釋放了所有的內存塊。
ü??????開始使用
?????????建立堆:
????????????使用以下API??
????????????HANDLE HeapCreate(DWORD?選項,SIZE_T?初始大小,SIZE_T?最大值)
“選項”?取值為0?,不是以下任意一個
HEAP_NO_SERIALIZE,系統無需同步堆
HEAP_GENERATE_EXCEPTIONS,當創建失敗或分配失敗時產生異常。
“初始大小”是堆的大小,系統會規整到頁面的整數倍,如0~4096的任何數都為4096;但是,進程空間至少要64K。
“最大值”是堆允許的最大值;為0則無限。
使用HEAP_NO_SERIALIZE需確定只有單線程訪問這個堆,否則有可能破壞堆;或程序有同步代碼來同步堆。
C++程序如下:
pHeap=(char*)GetProcessHeap();
printf("默認堆地址=%x/n",pHeap);
?
MEMORYSTATUS memStatus2;
????????????GlobalMemoryStatus(&memStatus2);
HANDLE hHeap=HeapCreate(HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS,1024*1024*50,0);
????????????char* pHeap=(char*)hHeap;
????????????printf("新建堆1地址=%x/n",pHeap);??
????????????if(hHeap==NULL)
????????????{
????????????????????????cout<<"創建堆失敗!"<<endl;
????????????}
????????????MEMORYSTATUS memStatus3;
????????????GlobalMemoryStatus(&memStatus3);
????????????cout<<"建立堆后:"<<endl;
cout<<"減少物理內存="<<memStatus2.dwAvailPhys-memStatus3.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus2.dwAvailPageFile-memStatus3.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="<<memStatus2.dwAvailVirtual-memStatus3.dwAvailVirtual<<endl<<endl;
?
HANDLE hHeap2=HeapCreate(HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS,1024*1024*10,0);
????????????char* pHeap2=(char*)hHeap2;
????????????printf("新建堆2地址=%x/n",pHeap2);
?
????????????結果如下:
????????????
當建立堆1時,它分配了50M的物理內存給堆使用;當建立堆2時,堆2的地址是0x04bc 0000=0x019c 0000+50*1024*1024.
????????分配內存:
????????????使用以下API
????????????PVOID HeapAlloc(HANDLE?堆句柄,DWORD?選項,SIZE_T?字節數)
????????????“選項”可以是,
????????????HEAP_ZERO_MEMORY,所有字節初始化為0
????????????HEAP_NO_SERIALIZE,堆這個內存區獨享
HEAP_GENERATE_EXCEPTIONS,產生異常。如果創建堆有了它就不用再設了。異常可能為:STATUS_NO_MEMOR(無足夠內存)和STATUS_ACCESS_VIOLATION(堆被破壞,分配失敗)。
?
C++程序如下:
GlobalMemoryStatus(&memStatus3);
PVOID pV=HeapAlloc(hHeap,
HEAP_ZERO_MEMORY|HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS,1024*507);
????????????if(pV==NULL)
????????????{
????????????????????????cout<<"分配堆內存失敗!"<<endl;
????????????}
????????????char?* pC=(char*)pV;
????????????printf("第一次分配地址=%x/n",pC);
???????????
????????????MEMORYSTATUS memStatus4;
????????????GlobalMemoryStatus(&memStatus4);
????????????cout<<"第一次堆分配后:"<<endl;
cout<<"減少物理內存="<<memStatus3.dwAvailPhys-memStatus4.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus3.dwAvailPageFile-memStatus4.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="<<memStatus3.dwAvailVirtual-memStatus4.dwAvailVirtual<<endl<<endl;
?
PVOID pV2=HeapAlloc(hHeap,
HEAP_ZERO_MEMORY|HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS,1024*508);
????????????if(pV2==NULL)
????????????{
????????????????????????cout<<"分配堆內存失敗!"<<endl;
????????????}
????????????char?* pC2=(char*)pV2;
????????????printf("第二次分配地址=%x/n",pC2);
????????????MEMORYSTATUS memStatus5;
????????????GlobalMemoryStatus(&memStatus5);
????????????cout<<"第二次堆分配后:"<<endl;
cout<<"減少物理內存="<<memStatus4.dwAvailPhys-memStatus5.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus4.dwAvailPageFile-memStatus5.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="<<memStatus4.dwAvailVirtual-memStatus5.dwAvailVirtual<<endl<<endl;
????????????for(int?i=0;i<200*1024;i++)
????????????????????????pC2[i]=9;
?
????????????MEMORYSTATUS memStatus10;
????????????GlobalMemoryStatus(&memStatus10);
????????????cout<<"第二次堆使用一半后:"<<endl;
cout<<"減少物理內存="<<memStatus5.dwAvailPhys-memStatus10.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus5.dwAvailPageFile-memStatus10.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="
<<memStatus5.dwAvailVirtual-memStatus10.dwAvailVirtual<<endl<<endl;
結果如下:
?
可以看出,第一次分配507K的地址為0x04ad d650<0x04bc 0000,它是在堆中分配的;第二次分配508K的地址為0x055c 0020>0x04bc 0000,它是在堆外分配的;無論在多大的堆中,只要分配內存塊大于507K時,都會在堆外分配,但是,它像在堆中一樣,存在堆的鏈接表中,受堆管理。分配時,系統使用的是虛擬頁文件;只有在真正使用時,才會分配物理內存。
至于為什么分配大于507K會在堆外分配而不直接使用堆中的內存,目前仍然不清楚。
?????????改變大小:
PVOID HeapReAlloc(HANDLE?堆句柄,DWORD?選項,PVOID?舊內存塊地址,SIZE_T?新內存塊大小)
“選項”除了以上三個外,還有HEAP_REALLOC_IN_PLACE_ONLY,指定不能移動原有內存塊的地址。
C++程序如下:
GlobalMemoryStatus(&memStatus4);
????????????PVOID pV2New=HeapReAlloc(hHeap,0,pV2,1024*1024*2);
????????????if(pV2New!=NULL)
????????????{
????????????char?* pC2New=(char*)pV2New;
????????????printf("改變分配地址=%x/n",pC2New);
????????????cout<<pC2New[0]<<endl;
????????????//cout<<pC2[0]<<endl;出現訪問違規
????????????SIZE_T lenNew=HeapSize(hHeap,0,pV2New);
????????????cout<<"改變后大小="<<lenNew<<endl;
????????????}
????????????GlobalMemoryStatus(&memStatus5);
????????????cout<<"改變分配后:"<<endl;
cout<<"減少物理內存="<<memStatus4.dwAvailPhys-memStatus5.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus4.dwAvailPageFile-memStatus5.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="
<<memStatus4.dwAvailVirtual-memStatus5.dwAvailVirtual<<endl<<endl;
結果如下:
?
可以看出,新內存塊緊接著原來內存塊結束的地方開始創建,大小為2M;原來的內存塊的內容被銷毀和釋放,所以新內存塊只減少了增加的內存量。一個缺點就是,新內存塊居然不保留原來內存的內容!另外,如果采用HEAP_REALLOC_IN_PLACE_ONLY的話,出現Not Enough Quote異常。也就是說,當前內存的狀況是,必須移動才可以擴大此內存塊。
?????????查詢內存:
????????????可以查詢堆中一個內存塊的大小。
????????????SIZE_T HeapSize(HANDLE?堆句柄,DWORD?選項,LPVOID?內存塊地址)
????????????“選項”可為0或HEAP_NO_SERIALIZE。
????????????參考以上例子。
?????????釋放內存塊:
????????????BOOL HeapFree(HANDLE?堆句柄,DWORD?選項,PVOID?內存塊地址)
????????????“選項”可為0或HEAP_NO_SERIALIZE。
????????????C++程序如下:
????????????GlobalMemoryStatus(&memStatus5);
????????????HeapFree(hHeap,0,pV2New);
????????????MEMORYSTATUS memStatus6;
????????????GlobalMemoryStatus(&memStatus6);
????????????cout<<"第二次堆分配釋放后:"<<endl;
cout<<"增加物理內存="<<memStatus6.dwAvailPhys-memStatus5.dwAvailPhys<<endl;
cout<<"增加可用頁文件="<<memStatus6.dwAvailPageFile-memStatus5.dwAvailPageFile<<endl;
cout<<"增加可用進程空間="<<memStatus6.dwAvailVirtual-memStatus5.dwAvailVirtual<<endl<<endl;
結果如下:
?
????????????內存空間釋放了原來的2M空間。
?????????釋放堆:
????????????BOOL HeapDestroy(HANDLE?堆句柄)
????????????不能用它釋放默認堆,系統忽略它的處理。
這一次,我們先在堆1中分配了70M的內存,由于它很大,所以,堆在堆外給它分配了內存,所以,堆1一共有50M+70M=120M。釋放程序如下:
PVOID pV4=HeapAlloc(hHeap,HEAP_ZERO_MEMORY|HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS)
,1024*1024*70);
????????????if(pV4==NULL)
????????????{
????????????????????????cout<<"分配堆內存失敗!"<<endl;
????????????}
????????????char?* pC4=(char*)pV4;
????????????printf("第四次堆分配=%x/n",pC4);
????????????MEMORYSTATUS memStatus9;
????????????GlobalMemoryStatus(&memStatus9);
????????????cout<<"分配堆內存后:"<<endl;
cout<<"減少物理內存="<<memStatus7.dwAvailPhys-memStatus9.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus7.dwAvailPageFile-memStatus9.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="<<memStatus7.dwAvailVirtual-memStatus9.dwAvailVirtual<<endl<<endl;
?
????????????SIZE_T len=HeapSize(hHeap,0,pV4);
????????????cout<<"len="<<len<<endl;
????????????bool?re=HeapDestroy(hHeap);
????????????if(re==false)
????????????{
????????????????????????cout<<"釋放堆失敗!"<<endl;
}
MEMORYSTATUS memStatus8;
????????????GlobalMemoryStatus(&memStatus8);
????????????cout<<"釋放堆后:"<<endl;
cout<<"增加物理內存="<<memStatus8.dwAvailPhys-memStatus9.dwAvailPhys<<endl;
cout<<"增加可用頁文件="<<memStatus8.dwAvailPageFile-memStatus9.dwAvailPageFile<<endl;
cout<<"增加可用進程空間="<<memStatus8.dwAvailVirtual-memStatus9.dwAvailVirtual<<endl<<endl;
?
結果如下:
?
如所猜想一樣,釋放了120M內存。
?
?????????獲取所有堆:
????????????DWORD GetProcessHeaps(DWORD?數量,PHANDLE?句柄數組)
????????????“數量”是你想獲取的堆數目;
????????????“句柄數組”是獲得的堆句柄。
????????????默認堆也可以獲取。
????????????HANDLE????????handles[10];
????????????memset(handles,0,sizeof(handles));
????????????GetProcessHeaps(10,handles);
????????????for(int?i=0;i<10;i++)
????????????????????????cout<<"堆"<<i+1<<"="<<handles[i]<<endl;
????????????結果如下:
????????????
可以看見,一共有8個堆,堆1是默認堆,堆7和堆8是本文建立的堆。另外5個不知來源。
?????????驗證堆:
????????????BOOL HeapValidate(HANDLE?堆句柄,DWORD?選項,LPVOID?內存塊地址)
????????????“選項”?可為0或HEAP_NO_SERIALIZE;
????????????“內存塊地址”為NULL時,驗證所有內存塊。
????????????C++程序如下:
HANDLE????????handles[10];
????????????memset(handles,0,sizeof(handles));
????????????GetProcessHeaps(10,handles);
????????????for(int?i=0;i<10;i++)
????????????{
????????????????????????cout<<"堆"<<i+1<<"="<<handles[i]<<"???";
????????????????????????if(HeapValidate(handles[i],0,NULL))
????????????????????????????????????cout<<"驗證堆成功!"<<endl;
????????????????????????else
????????????????????????????????????cout<<endl;
?
????????????}
結果如下:
?
????????合并內存塊:
????????????UINT HeapCompact(HANDLE?堆句柄,DWORD?選項)
????????????“選項”?可為0或HEAP_NO_SERIALIZE;
????????????此函數可以合并空閑內存塊。
???????
????????其他函數:
????????????HeapLock和HeapUnlock?通常是系統使用的;
????????????HeapWalk可以遍歷堆內存,需要以上兩個函數。
???????????
·????????C++內存函數
Malloc和Free
這是C語言使用的函數,只能從默認堆中分配內存,并且只是分配內存,不能調用構造函數,且只是按字節分配,不能按類型分配。
New?和Delete
這是C++語言使用的函數,默認情況下從默認堆中分配內存,但是也可以通過重載New函數,從自建堆中按類型分配;同時可以執行構造函數和析構函數。它底層是通過HeapAlloc和HeapFree實現的。?依賴于編譯器的實現。
GlobalAlloc?和GlobalFree
這是比HeapAlloc和HeapFree更慢的函數,但是也沒有比它們更好的優點,只能在默認堆中分配;16位操作系統下利用它們分配內存。
LocalAlloc和LocalFree
在WindowsNT?內核里,和GlobalAlloc、GlobalFree是一樣的。
?
·????????一個例子
默認情況下,New關鍵字是利用HeapAlloc在默認堆上建立對象。本文重載了類的New方法,使得類在自己的堆中存放,這樣可以與外面的對象隔離,以免重要的數據結構被意外破壞。由于類中的成員變量是在堆中存放,因此不局限于線程堆棧的1M空間。
C++程序如下:
class?AllocateInOtherHeap
{
public:
????????????AllocateInOtherHeap(void);
????????????~AllocateInOtherHeap(void);
????????????void*?operator?new(size_t size);
????????????static?HANDLE heap;
?????????public:
????????????//類對象唯一所需的空間
????????????int?iArray[1024*1024*10];
????????????AllocateInOtherHeap::AllocateInOtherHeap(void)
{
????????????cout<<"AllocateInOtherHeap()"<<endl;
????????????//如果New函數沒有分配夠空間,那么此處會出現訪問違規
????????????memset(iArray,0,sizeof(AllocateInOtherHeap));
????????????iArray[1024]=8;
}
void* AllocateInOtherHeap::operator?new(size_t size)
{?????????
????????????if(heap==NULL)
heap=HeapCreate(HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS,1024*1024*10,0);
????????????//分配足夠這個類對象的空間
????????????void* p=HeapAlloc(heap,0,sizeof(AllocateInOtherHeap));
????????????cout<<"堆的大小="<<HeapSize(heap,0,p)<<endl;
????????????printf("AllocateInOtherHeap堆地址=%x/n",heap);
????????????printf("AllocateInOtherHeap返回地址=%x/n",p);
????????????return?p;
}
AllocateInOtherHeap::~AllocateInOtherHeap(void)
{
????????????cout<<"~AllocateInOtherHeap"<<endl;
}
void?AllocateInOtherHeap::operator?delete(void* p)
{?????????
????????????HeapFree(heap,0,p);
????????????HeapDestroy(heap);
????????????cout<<"delete()"<<endl;??????????
}
};
結果如下:
可見,new函數先分配夠空間,然后才能初始化對象變量;而delete函數得先做析構,才能釋放空間。對象保存在堆外,因為大于512K;對象大小剛好是iArray變量的大小。
注意,如果沒有分配足夠的空間,雖然你可以得到對象指針,但是你訪問數據時可能會出現訪問違規,如果沒出現,那更慘,意味著你讀寫了別人的數據。
新人創作打卡挑戰賽發博客就能抽獎!定制產品紅包拿不停!總結
以上是生活随笔為你收集整理的全面介绍Windows内存管理机制及C++内存分配实例(五):堆的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【bzoj1486】【[HNOI2009
- 下一篇: Java版单链表讲解