Windows編程中的堆管理

來源:互聯網
上載者:User

摘要: 本文主要對Windows記憶體管理中的堆管理技術進行討論,並簡要介紹了堆的建立、記憶體塊的分配與再分配、堆的撤銷以及new和delete操作符的使用等內容。

關鍵詞: 堆;堆管理
  1 引言

  在大多數Windows應用程式設計中,都幾乎不可避免的要對記憶體進行操作和管理。在進行大尺寸記憶體的動態分配時尤其顯的重要。本文即主要對記憶體管理中的堆管理技術進行論述。

   堆(Heap)實際是位於保留的虛擬位址空間中的一個地區。剛開始時,保留地區中的多數頁面並沒有被提交實體儲存體器。隨著從堆中越來越多的進行記憶體分 配,堆管理器將逐漸把更多的實體儲存體器提交給堆。堆的實體儲存體器從系統頁檔案中分配,在釋放時有專門的堆管理器負責對已佔用實體儲存體器的回收。堆管理也是 Windows提供的一種記憶體管理機制。主要用來分配小的資料區塊。與Windows的其他兩種記憶體管理機制虛擬記憶體和記憶體對應檔相比,堆可以不必考慮諸 如系統的分配粒度和頁面邊界之類比較煩瑣而又容易忽視的問題,可將注意力集中於對程式功能代碼的設計上。但是使用堆去分配、釋放記憶體的速度要比其他兩種機 制慢的多,而且不具備直接控制實體儲存體器提交與回收的能力。

  在進程剛啟動時,系統便在剛建立的進程虛擬位址空間中建立了一個堆,該堆 即為進程的預設堆,預設大小為1MB,該值允許在連結程式時被更改。進程的預設堆是比較重要的,可供眾多Windows函數使用。在使用時,系統必須保證 在規定的時間內,每此只有一個線程能夠分配和釋放預設堆中的記憶體塊。雖然這種限制將會對訪問速度產生一定的影響,但卻可以保證進程中的多個線程在同時調用 各種Windows函數時對預設堆的順序訪問。在進程中允許使用多個堆,進程中包括預設堆在內的每個堆都有一個堆控制代碼來標識。與自己建立的堆不同,進程默 認堆的建立、銷毀均由系統來完成,而且其生命期早在進程開始執行之前就已經開始,雖然在程式中可以通過GetProcessHeap()函數得到進程的默 認堆控制代碼,但卻不允許調用HeapDestroy()函數顯式將其撤消。

2 對動態建立堆的需求

  前面 曾提到,在進程中除了進程預設堆外,還可以在進程虛擬位址空間中動態建立一些獨立的堆。至於在程式設計時究竟需不需要動態建立獨立的堆可以從是否有保護群組 件的需要、是否能更加有效地對記憶體進行管理、是否有進行本地訪問的需要、是否有減少線程同步開銷的需要以及是否有迅速釋放堆的需要等幾個方面去考慮。

   對於是否有保護群組件的需要這一原則比較容易理解。在圖1中,左邊的圖表示了一個鏈表(節點結構)組件和一個樹(分支結構)組件共同使用一個堆的情況。在 這種情況下,由於兩組件資料在堆中的混合存放,如果節點3(屬於鏈表組件)的後幾個位元組由於被錯誤改寫,將有可能影響到位於其後的分支2(屬於樹組件)。 這將致使樹組件的相關代碼在遍曆其樹時由於記憶體被破壞而無法進行。究其原因,樹組件的記憶體是由於鏈表組建對其自身的錯誤操作而引起的。如果採用右圖所示方 式,將樹組件和鏈表組件分別存放於一個獨立的堆中,上述情況顯然不會發生,錯誤將被局限於進行了錯誤操作的鏈表組件,而樹組件由於存放在獨立的堆中而受到 了保護。

 
圖1 動態建立堆在保護群組件中的作用

   在中,如果鏈表組件的每個節點佔用12個位元組,每個樹組件的分支佔用16個位元組如果這些長度不一的對象共用一個堆(左圖),在左圖中這些已經分配了 記憶體的對象已佔滿了堆,如果其中有節點2和節點4釋放,將會產生24個位元組的片段,如果試圖在24個位元組的空閑區間內分配一個16位元組的分支對象,儘管要 分配的位元組數小於空閑位元組數,但分配仍將失敗。只有在堆棧中分配大小相同的對象才可以實行更加有效記憶體管理。如果將樹組件換成其他長度為12位元組的組 件,那麼在釋放一個對象後,另一個對象就可以恰好填充到此剛釋放的對象空間中。

  進行本地訪問的需要也是一條比較重要的原則。系統會經 常在記憶體與系統頁檔案之間進行頁面交換,但如果交換次數過多,系統的運行效能就將受很大的影響。因此在程式設計時應盡量避免系統頻繁交換頁面,如果將那些 會被同時訪問到的資料分配在相互靠近的位置上,將會減少系統在記憶體和頁檔案之間的頁面交換頻率。

  線程同步開銷指的是預設條件下以順序 方式啟動並執行堆為保護資料在多個線程試圖同時訪問時不受破壞而必須執行額外代碼所花費的開銷。這種開銷保證了堆對線程的安全性,因此是有必要的,但對於大量 的堆分配操作,這種額外的開銷將成為一個負擔,並降低程式的運行效能。為避免這種額外的開銷,可以在建立新堆時通知系統只有單個線程對訪問。此時堆對線程 的安全性將有應用程式來負責。

  最後如果有迅速釋放堆的需要,可將專用堆用於某些資料結構,並以整個堆去釋放,而不再顯式地釋放在堆中分配的每一個記憶體塊。對於大多數應用程式,這樣的處理將能以更快的速度運行。

  3 建立堆

  在進程中,如果需要可以在原有預設堆的基礎上動態建立一個堆,可由HeapCreate()函數完成:

HANDLE HeapCreate(
 DWORD flOptions,
 DWORD dwInitialSize,
 DWORD dwMaximumSize
);

  其第一個參數flOptions指定了對建立堆的操作屬性。該標誌將會影響一些堆函數如HeapAlloc()、HeapFree()、HeapReAlloc()和HeapSize()等對建立堆的訪問。其可能的取值為下列標誌及其組合:

屬性標誌 說明
HEAP_GENERATE_EXCEPTIONS 在遇到由於記憶體越界等而引起的函數失敗時,由系統拋出一個異常來指出此失敗,而不是簡單的返回NULL指標。
HEAP_NO_SERIALIZE 指明互斥現象不會出現

  參數dwInitialSize和 dwMaximumSize分別為堆的初始大小和堆棧的最大尺寸。其中,dwInitialSize的值決定了最初提交給堆的位元組數。如果設定的數值不是 頁面大小的整數倍,則將被圓整到鄰近的頁邊界處。而dwMaximumSize則實際上是系統能為堆保留的地址空間地區的最大位元組數。如果該值為0,那麼 將建立一個可擴充的堆,堆的大小僅受可用記憶體的限制。如果應用程式需要分配大的記憶體塊,通常要將該參數設定為0。如果dwMaximumSize大於0, 則該值限定了堆所能建立的最大值,HeapCreate()同樣也要將該值圓整到鄰近的頁邊界,然後再在進程的虛擬位址空間為堆保留該大小的一塊地區。在 這種堆中分配的記憶體塊大小不能超過0x7FFF8位元組,任何試圖分配更大記憶體塊的行為將會失敗,即使是設定的堆大小足以容納該記憶體塊。如果 HeapCreate()成功執行,將會返回一個標識新堆的控制代碼,並可供其他堆函數使用。

  需要特別說明的是,在設定第一個參數時,對 HEAP_NO_SERIALIZE的標誌的使用要謹慎,一般應避免使用該標誌。這是同後續將要進行的堆函數HeapAlloc()的執行過程有關係的, 在HeapAlloc()試圖從堆中分配一個記憶體塊時,將執行下述幾步操作:

  1) 遍曆分配的和釋放的記憶體塊的連結資料表

  2) 搜尋一個空閑記憶體塊的地址

  3) 通過將空閑記憶體塊標記為"已指派"來分配新記憶體塊

  4) 將新分配的記憶體塊添加到記憶體塊列表

   如果這時有兩個線程1、2試圖同時從一個堆中分配記憶體塊,那麼線程1在執行了上面的1和2步後將得到空間記憶體塊的地址。但是由於CPU對線程已耗用時間的 分區,使得線程1在執行第3步操作前有可能被線程2搶走執行權並有機會去執行同樣的1、2步操作,而且由於先執行的線程1並沒有執行到第3步,因此線程2 會搜尋到同一個空閑記憶體塊的地址,並將其標記為已指派。而線程1在恢複運行後並不能知曉該記憶體塊已被線程2標記過,因此會出現兩個線程軍認為其分配的是空 閑的記憶體塊,並更新各自的聯結表。顯然,象這種兩個線程擁有完全相同記憶體塊地址的錯誤是非常嚴重而又是難以發現的。

  由於只有在多個線 程同時進行操作時才有可能出現上述問題,一種簡單的解決的辦法就是不使用HEAP_NO_SERIALIZE標誌而只允許單個線程獨佔地對堆及其聯結表擁 有訪問權。如果一定要使用此標誌,為了安全起見,必須確保進程為單線程的或是在進程中使用了多線程,但只有單個線程對堆進行訪問。再就是使用了多線程,也 有多個線程對堆進行了訪問,但這些線程通過使用某種線程同步手段。如果可以確保以上幾條中的一條成立,也是可以安全使用 HEAP_NO_SERIALIZE標誌的,而且還將擁有快的訪問速度。如果不能肯定上述條件是否滿足,建議不使用此標誌而以順序的方式訪問堆,雖然線程 速度會因此而下降但卻可以確保堆及其中資料的不被破壞。

  4 從堆中分配記憶體塊

  在成功建立一個堆後,可以調用HeapAlloc()函數從堆中分配記憶體塊。在此,除了可以從用HeapCreate()建立的動態堆中分配記憶體塊,也可以直接從進程的預設堆中分配記憶體塊。下面先給出HeapCreate()的函數原型:

LPVOID HeapAlloc(
 HANDLE hHeap,
 DWORD dwFlags,
 DWORD dwBytes
);

   其中,參數hHeap為要分配的記憶體塊來自的堆的控制代碼,可以是從HeapCreate()建立的動態堆控制代碼也可以是由GetProcessHeap() 得到的預設堆控制代碼。參數dwFlags指定了影響堆分配的各個標誌。該標誌將覆蓋在調用HeapCreate()時所指定的相應標誌,可能的取值為:

標誌 說明
HEAP_GENERATE_EXCEPTIONS 該標誌指定在進行諸如記憶體越界操作等情況時將拋出一個異常而不是簡單的返回NULL指標
HEAP_NO_SERIALIZE 強制對HeapAlloc()的調用將與訪問同一個堆的其他線程不按照順序進行
HEAP_ZERO_MEMORY 如果使用了該標誌,新分配記憶體的內容將被初始化為0

  最後一個參數 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編譯通過

摘自:http://www.chinavideo.org/index.php?option=com_content&task=view&id=292&Itemid=5

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.