1 引言html
在大多數Windows應用程序設計中,都幾乎不可避免的要對內存進行操做和管理。在進行大尺寸內存的動態分配時尤爲顯的重要。本文即主要對內存管理中的堆管理技術進行論述。windows
堆(Heap)實際是位於保留的虛擬地址空間中的一個區域。剛開始時,保留區域中的多數頁面並無被提交物理存儲器。隨着從堆中愈來愈多的進行內存分配,堆管理器將逐漸把更多的物理存儲器提交給堆。堆的物理存儲器從系統頁文件中分配,在釋放時有專門的堆管理器負責對已佔用物理存儲器的回收。堆管理也是Windows提供的一種內存管理機制。主要用來分配小的數據塊。與Windows的其餘兩種內存管理機制(1)虛擬內存和(2)內存映射文件相比,堆能夠沒必要考慮諸如系統的分配粒度和頁面邊界之類比較煩瑣而又容易忽視的問題,可將注意力集中於對程序功能代碼的設計上。可是使用堆去分配、釋放內存的速度要比其餘兩種機制慢的多,並且不具有直接控制物理存儲器提交與回收的能力。數組
在進程剛啓動時,系統便在剛建立的進程虛擬地址空間中建立了一個堆,該堆即爲進程的默認堆,缺省大小爲1MB,該值容許在連接程序時被更改(即,在VS編譯程序的時候,在項目設置-鏈接器設置-中能夠設置)。進程的默認堆是比較重要的,可供衆多Windows函數使用。在使用時,系統必須保證在規定的時間內,每此只有一個線程可以分配和釋放默認堆中的內存塊。雖然這種限制將會對訪問速度產生必定的影響,但卻能夠保證進程中的多個線程在同時調用各類Windows函數時對默認堆的順序訪問。安全
在進程中容許使用多個堆,進程中包括默認堆在內的每一個堆都有一個堆句柄來標識。與本身建立的堆不一樣,進程默認堆的建立、銷燬均由系統來完成,並且其生命期早在進程開始執行以前就已經開始,雖然在程序中能夠經過GetProcessHeap()函數獲得進程的默認堆句柄,但卻不容許調用HeapDestroy()函數顯式將其撤消。數據結構
2. 對動態建立堆的需求多線程
前面曾提到,在進程中除了進程默認堆外,還能夠在進程虛擬地址空間中動態建立一些獨立的堆。至於在程序設計時究竟需不須要動態建立獨立的堆能夠從是否有保護組件的須要、是否能更加有效地對內存進行管理、是否有進行本地訪問的須要、是否有減小線程同步開銷的須要以及是否有迅速釋放堆的須要等幾個方面去考慮。
(1)對因而否有保護組件的須要這一原則比較容易理解。在圖1中,左邊的圖表示了一個鏈表(節點結構)組件和一個樹(分支結構)組件共同使用一個堆的狀況。在這種狀況下,因爲兩組件數據在堆中的混合存放,若是節點3(屬於鏈表組件)的後幾個字節因爲被錯誤改寫,將有可能影響到位於其後的分支2(屬於樹組件)。這將導致樹組件的相關代碼在遍歷其樹時因爲內存被破壞而沒法進行。究其緣由,樹組件的內存是因爲鏈表組建對其自身的錯誤操做而引發的。若是採用右圖所示方式,將樹組件和鏈表組件分別存放於一個獨立的堆中,上述狀況顯然不會發生,錯誤將被侷限於進行了錯誤操做的鏈表組件,而樹組件因爲存放在獨立的堆中而受到了保護。函數
【圖1】性能
在上圖中,若是鏈表組件的每一個節點佔用12個字節,每一個樹組件的分支佔用16個字節若是這些長度不一的對象共用一個堆(左圖),在左圖中這些已經分配了內存的對象已佔滿了堆,若是其中有節點2和節點4釋放,將會產生24個字節的碎片,若是試圖在24個字節的空閒區間內分配一個16字節的分支對象,儘管要分配的字節數小於空閒字節數,但分配仍將失敗。只有在堆棧中分配大小相同的對象才能夠實行更加有效的內存管理。若是將樹組件換成其餘長度爲12字節的組件,那麼在釋放一個對象後,另外一個對象就能夠剛好填充到此剛釋放的對象空間中。
(2)進行本地訪問的須要也是一條比較重要的原則。系統會常常在內存與系統頁文件之間進行頁面交換,但若是交換次數過多,系統的運行性能就將受很大的影響。所以在程序設計時應儘可能避免系統頻繁交換頁面,若是將那些會被同時訪問到的數據分配在相互靠近的位置上,將會減小系統在內存和頁文件之間的頁面交換頻率。
(3)線程同步開銷指的是默認條件下以順序方式運行的堆爲保護數據在多個線程試圖同時訪問時不受破壞而必須執行額外代碼所花費的開銷。這種開銷保證了堆對線程的安全性,所以是有必要的,但對於大量的堆分配操做,這種額外的開銷將成爲一個負擔,並下降程序的運行性能。爲避免這種額外的開銷,能夠在建立新堆時通知系統只有單個線程對訪問。此時堆對線程的安全性將有應用程序來負責。
(4)最後若是有迅速釋放堆的須要,可將專用堆用於某些數據結構,並以整個堆去釋放,而再也不顯式地釋放在堆中分配的每個內存塊。對於大多數應用程序,這樣的處理將能以更快的速度運行。spa
3. 建立堆線程
在進程中,若是須要能夠在原有默認堆的基礎上動態建立一個堆,可由HeapCreate()函數完成:
HANDLE HeapCreate(
DWORD flOptions,
DWORD dwInitialSize,
DWORD dwMaximumSize
);
其第一個參數flOptions指定了對新建堆的操做屬性。該標誌將會影響一些堆函數如HeapAlloc()、HeapFree()、HeapReAlloc()和HeapSize()等對新建堆的訪問。其可能的取值爲下列標誌及其組合:
參數dwInitialSize和dwMaximumSize分別爲堆的初始大小和堆棧的最大尺寸。其中,dwInitialSize的值決定了最初提交給堆的字節數。若是設置的數值不是頁面大小的整數倍,則將被圓整(Round Up)到鄰近的頁邊界處。而dwMaximumSize則其實是系統能爲堆保留的地址空間區域的最大字節數。如果該值爲0,那麼將建立一個可擴展的堆,堆的大小僅受可用內存的限制。若是應用程序須要分配大的內存塊,一般要將該參數設置爲0。若是dwMaximumSize大於0,則該值限定了堆所能建立的最大值,HeapCreate()一樣也要將該值圓整到鄰近的頁邊界,而後再在進程的虛擬地址空間爲堆保留該大小的一塊區域。在這種堆中分配的內存塊大小不能超過0x7FFF8字節,任何試圖分配更大內存塊的行爲將會失敗,即便是設置的堆大小足以容納該內存塊。
若是HeapCreate()成功執行,將會返回一個標識新堆的句柄,並可供其餘堆函數使用。
須要特別說明的是,在設置第一個參數時,對HEAP_NO_SERIALIZE的標誌的使用要謹慎,通常應避免使用該標誌。這是同後續將要進行的堆函數HeapAlloc()的執行過程有關係的,在HeapAlloc()試圖從堆中分配一個內存塊時,將執行下述幾步操做:
1) 遍歷分配的和釋放的內存塊的連接表
2) 搜尋一個空閒內存塊的地址
3) 經過將空閒內存塊標記爲"已分配"來分配新內存塊
4) 將新分配的內存塊添加到內存塊列表
如果這時有兩個線程一、2試圖同時從一個堆中分配內存塊,那麼線程1在執行了上面的1和2步後將獲得空間內存塊的地址。但是因爲CPU對線程運行時間的分片,使得線程1在執行第3步操做前有可能被線程2搶走執行權並有機會去執行一樣的一、2步操做,並且因爲先執行的線程1並無執行到第3步,所以線程2會搜尋到同一個空閒內存塊的地址,並將其標記爲已分配。而線程1在恢復運行後並不能知曉該內存塊已被線程2標記過,所以會出現兩個線程軍認爲其分配的是空閒的內存塊,並更新各自的聯接表。顯然,象這種兩個線程擁有徹底相同內存塊地址的錯誤是很是嚴重而又是難以發現的。
因爲只有在多個線程同時進行操做時纔有可能出現上述問題,一種簡單的解決的辦法就是不使用HEAP_NO_SERIALIZE標誌而只容許單個線程獨佔地對堆及其聯接表擁有訪問權。若是必定要使用此標誌,爲了安全起見,必須確保進程爲單線程的或是在進程中使用了多線程,但只有單個線程對堆進行訪問。再就是使用了多線程,也有多個線程對堆進行了訪問,但這些線程經過使用某種線程同步手段。若是能夠確保以上幾條中的一條成立,也是能夠安全使用HEAP_NO_SERIALIZE標誌的,並且還將擁有快的訪問速度。若是不能確定上述條件是否知足,建議不使用此標誌而以順序的方式訪問堆,雖然線程速度會所以而降低但卻能夠確保堆及其中數據的不被破壞。
4. 從堆中分配內存塊
在成功建立一個堆後,能夠調用HeapAlloc()函數從堆中分配內存塊。
在此,該函數能夠從兩個種堆中分配內存塊。(1)從用HeapCreate()建立的動態堆中分配內存塊,(2)也能夠直接從進程的默認堆中分配內存塊。
下面先給出HeapCreate()的函數原型
LPVOID HeapAlloc(
HANDLE hHeap,
DWORD dwFlags,
DWORD dwBytes
);
其中,參數hHeap爲要分配的內存塊來自的堆的句柄(從分配的哪一個堆中進行份內存塊),能夠是從HeapCreate()建立的動態堆句柄也能夠是由GetProcessHeap()獲得的默認堆句柄。
參數dwFlags指定了影響堆分配的各個標誌。該標誌將覆蓋在調用HeapCreate()時所指定的相應標誌,可能的取值爲:
最後一個參數dwBytes設定了要從堆中分配的內存塊的大小。若是HeapAlloc()執行成功,將會返回從堆中分配的內存塊的地址。若是因爲內存不足或是其餘一些緣由而引發HeapAlloc()函數的執行失敗,將會引起異常。經過異常標誌能夠獲得引發內存分配失敗的緣由:若是爲STATUS_NO_MEMORY則代表是因爲內存不足引發的;若是是STATUS_Access_VIOLATION則表示是因爲堆被破壞或函數參數不正確而引發分配內存塊的嘗試失敗。以上異常只有在指定了HEAP_GENERATE_EXCEPTIONS標誌時纔會發生,若是沒有指定此標誌,在出現相似錯誤時HeapAlloc()函數只是簡單的返回NULL指針。
在設置dwFlags參數時,若是先前用HeapCreate()建立堆時曾指定過HEAP_GENERATE_EXCEPTIONS標誌,就沒必要再去設置HEAP_GENERATE_EXCEPTIONS標誌了,由於HEAP_GENERATE_EXCEPTIONS標誌已經通知堆在不能分配內存塊時將會引起異常。另外,對HEAP_NO_SERIALIZE標誌的設置應慎重,與在HeapCreate()函數中使用HEAP_NO_SERIALIZE標誌相似,若是在同一時間有其餘線程使用同一個堆,那麼該堆將會被破壞。若是是在進程默認堆中進行內存塊的分配則要絕對禁用此標誌。
在使用堆函數HeapAlloc()時要注意:堆在內存管理中的使用主要是用來分配一些較小的數據塊,若是要分配的內存塊在1MB左右,那麼就不要再使用堆來管理內存了,而應選擇虛擬內存的內存管理機制。
5. 再分配內存塊
在程序設計時常常會因爲開始時預見不足而形成在堆中分配的內存塊大小的不合適(多數狀況是開始時分配的內存較小,然後來實際須要更多的數據複製到內存塊中去)這就須要在分配了內存塊後再根據須要調整其大小。堆函數HeapReAlloc()將完成這一功能,其函數原型爲:
LPVOID HeapReAlloc(
HANDLE hHeap,
DWORD dwFlags,
LPVOID lpMem,
DWORD dwBytes
);
其中,參數hHeap爲包含要調整其大小的內存塊的堆的句柄。
dwFlags參數指定了在更改內存塊大小時HeapReAlloc()函數所使用的標誌。其可能的取值爲HEAP_GENERATE_EXCEPTIONS、HEAP_NO_SERIALIZE、HEAP_REALLOC_IN_PLACE_ONLY和HEAP_ZERO_MEMORY,其中前兩個標誌的做用與在HeapAlloc()中的做用相同。HEAP_REALLOC_IN_PLACE_ONLY標誌在內存塊被加大時不移動堆中的內存塊,在沒有設置此標誌的狀況下若是對內存進行增大,那麼HeapReAlloc()函數將有可能將原內存塊移動到一個新的地址。顯然,在設置了該標誌禁止內存快首地址進行調整時,將有可能出現沒有足夠的內存供試圖增大的內存塊使用,對於這種狀況,函數對內存塊增大調整的操做是失敗的,內存塊將仍保留原有的大小和位置。HEAP_ZERO_MEMORY標誌的用處則略有不一樣,若是內存快通過調整比之前大,那麼新增長的那部份內存將被初始化爲0;若是通過調整內存塊縮小了,那麼該標誌將不起任何做用。
函數的最後兩個參數lpMem和dwBytes分別爲指向再分配內存塊的指針和再分配的字節數。若是函數成功執行,將返回新的改變了大小的內存塊的地址。若是在調用時使用了HEAP_REALLOC_IN_PLACE_ONLY標誌,那麼返回的地址將與原內存塊地址相同。若是由於內存不足等緣由而引發函數的執行失敗,函數將返回一個NULL指針。可是HeapReAlloc()的執行失敗並不會影響原內存塊,它將保持原來的大小和位置繼續存在。能夠經過HeapSize()函數來檢索內存塊的實際大小。
6. 釋放堆內存、撤消堆
在再也不須要使用堆中的內存塊時,能夠經過HeapFree()將其予以釋放。該函數結構比較簡單,只含有三個參數:
BOOL HeapFree(
HANDLE hHeap,
DWORD dwFlags,
LPVOID lpMem
);
其中,hHeap爲要包含要釋放內存塊的堆的句柄;參數dwFlags爲堆棧的釋放選項能夠是0,也能夠是HEAP_NO_SERIALIZE;最後的參數lpMem爲指向內存塊的指針。若是函數成功執行,將釋放指定的內存塊,並返回TRUE。該函數的主要做用是能夠用來幫助堆管理器回收某些不使用的物理存儲器以騰出更多的空閒空間,可是並不能保證必定會成功。
最後,在程序退出前或是應用程序再也不須要其建立的堆了,能夠調用HeapDestory()函數將其銷燬。該函數只包含一個參數--待銷燬的堆的句柄。HeapDestory()的成功執行將能夠釋放堆中包含的全部內存塊,也可將堆佔用的物理存儲器和保留的地址空間區域所有從新返回給系統並返回TRUE。該函數只對由HeapCreate()顯式建立的堆起做用,而不能銷燬進程的默認堆,若是強行將由GetProcessHeap()獲得的默認堆的句柄做爲參數去調用HeapDestory(),系統將會忽略對該函數的調用。
7 對new與delete操做符的重載
new與delete內存空間動態分配操做符是C++中使用堆進行內存管理的一種經常使用方式,在程序運行過程當中能夠根據須要隨時經過這兩個操做符創建或刪除堆對象。
new操做符將在堆中分配一個足夠大小的內存塊以存放指定類型的對象,若是每次構造的對象類型不一樣,則須要按最大對象所佔用的空間來進行分配。new操做符在成功執行後將返回一個類型與new所分配對象相匹配的指針,若是不匹配則要對其進行強制類型轉換,不然將會編譯出錯。在再也不須要這個對象的時候,必須顯式調用delete操做符來釋放此空間。這一點是很是重要的,若是在預分配的緩衝裏構造另外一個對象以前或者在釋放緩衝以前沒有顯式調用delete操做符,那麼程序將產生不可預料的後果。
在使用delete操做符時,應注意如下幾點:
1) 它必須使用於由運算符new返回的指針
2) 該操做符也適用於NULL指針
3) 指針名前只用一對方括號符,而且無論所刪除數組的維數,忽略方括號內的任何數字
class CVMShow{ private: static HANDLE m_sHeap; static int m_sAllocedInHeap; public: LPVOID operator new(size_t size); void operator delete(LPVOID pVoid); } …… HANDLE m_sHeap = NULL; int m_sAllocedInHeap = 0;
LPVOID CVMShow::operator new(size_t size) { if (m_sHeap == NULL) m_sHeap = HeapCreate(HEAP_GENERATE_EXCEPTIONS, 0, 0); LPVOID pVoid = HeapAlloc(m_sHeap, 0, size); if (pVoid == NULL) return NULL; m_sAllocedInHeap++; return pVoid; } void CVMShow::operator delete(LPVOID pVoid) { if (HeapFree(m_sHeap, 0, pVoid)) m_sAllocedInHeap--; if (m_sAllocedInHeap == 0) { if (HeapDestory(m_sHeap)) m_sHeap = NULL; } }
在程序中除了直接用上述方法使用new和delete來創建和刪除堆對象外,還能夠經過爲C++類重載new和delete操做符來方便地利用堆棧函數。
上面的代碼對它們進行了簡單的重載,並經過靜態變量m_sHeap和m_sAllocedInHeap在類CVMShow的全部實例間共享惟一的堆句柄(由於在這裏CVMShow類的全部實例都是在同一個堆中進行內存分配的)和已分配類對象的計數。這兩個靜態變量在代碼開始執行時被分別初始化爲NULL指針和0計數。
重載的new操做符在第一次被調用時,因爲靜態變量m_sHeap爲NULL標誌着堆還沒有建立,就經過HeapCreate()函數建立一個堆並返回堆句柄到m_sHeap。隨後根據入口參數size所指定的大小在堆中分配內存,同時已分配內存塊計數器m_sAllocedInHeap累加。在該操做符的之後調用過程當中,因爲堆已經建立,故再也不建立堆,而是直接在堆中分配指定大小的內存塊並對已分配的內存塊個數進行計數。
在CVMShow類對象再也不被應用程序所使用時,須要將其撤消,由重載的delete操做符完成此工做。delete操做符只接受一個LPVOID型參數,即被刪除對象的地址。該函數在執行時首先調用HeapFree()函數將指定的已分配內存的對象釋放並對已分配內存計數遞減1。若是該計數不爲零則代表當前堆中的內存塊沒有所有釋放,堆暫時不予撤消。若是m_sAllocedInHeap計數減到0,則堆中已釋放完全部的CVMShow對象,能夠調用HeapDestory()函數將堆銷燬,並將堆句柄m_sHeap設置爲NULL指針。這裏在撤消堆後將堆句柄設置爲NULL指針的操做是徹底必要的。若是不執行該操做,當程序再次調用new操做符去分配一個CVMShow類對象時將會認爲堆是存在的而會試圖在已撤消的堆中去分配內存,顯然將會致使失敗。
象CVMShow這樣設計的類經過對new和delete操做符的重載,而且在一個堆中爲全部的CVMShow類對象進行分配,能夠節省在爲每個類都建立堆的分配開銷與內存。這樣的處理還可讓每個類都擁有屬於本身的堆,而且容許派生類對其共享,這在程序設計中也是比較好的一種處理方法。
8 小結
在使用堆時有時會形成系統運行速度的減慢,一般是由如下緣由形成的:
分配操做形成的速度減慢;釋放操做形成的速度減慢;堆競爭形成的速度減慢;堆破壞形成的速度減慢;頻繁的分配和重分配形成的速度減慢等。其中,競爭是在分配和釋放操做中致使速度減慢的問題。
基於上述緣由,建議不要在程序中過於頻繁的使用堆。文中所述代碼均在Windows 2000 Professional下由Microsoft Visual C++ 6.0編譯經過。
參考: