從幾個指標談windows記憶體

查志強發表於2015-01-08

【原文:http://bbs.pediy.com/showthread.php?t=156036

在windows系統上,關於記憶體洩漏我們通常會聽到這麼兩句話:
1. 藉助效能監視器,Private Bytes和Virtual Bytes至少有一個是一條斜向上曲線,大多數洩漏是這種情況;
2. 如果Private Byte和Virtual Bytes一起上升,但是後者比前者上升得快或者比例超過3:1,說明不僅僅有記憶體洩漏,而且洩漏導致了記憶體碎片
 
      但關鍵的是,這些指標到底是什麼意思,這幾個指標的這些變化趨勢真的就能反映出程式有記憶體洩漏問題?如果能為什麼能等等問題,其實沒有多少人能夠真正說得清楚,本文就試圖通過這些指標入手,談談windows記憶體相關技術知識,但也不準備深入到核心層次深談記憶體管理機制,只是會涉及我們平時涉及最多概念背後的故事。
 
      首先還是要普及一下老生常談:“在Windows系統中,任何一個程式都被賦予其自己的虛擬地址空間,該虛擬地址空間覆蓋了一個相當大的範圍,對於32位程式,其地址空間為232=4,294,967,296 Byte (4G),這使得一個指標可以使用從0x00000000到0xFFFFFFFF的4GB範圍之內的任何一個值。雖然每一個32位程式可使用4GB的地址空間,但並不意味著每一個程式實際擁有4GB的實體地址空間,該地址空間僅僅是一個虛擬地址空間,此虛擬地址空間只是記憶體地址的一個範圍。程式的虛擬地址空間是為每個程式所私有的,在程式內執行的執行緒對記憶體空間的訪問都被限制在呼叫程式之內,而不能訪問屬於其他程式的記憶體空間。這樣,在不同的程式中可以使用相同地址的指標來指向屬於各自呼叫程式的內容而不會由此引起混亂。” 
每個程式看到得虛擬地址空間有大量準確定義的區(area)構成,每個區都有專門的功能。從最低的地址看起:
      • 程式程式碼和資料:程式碼是從同一固定地址開始,緊接著的是和C全域性變數相對應的資料區。 
      • 堆:程式碼和資料區後緊隨著的是執行時堆。作為呼叫malloc和free這樣的C標準庫函式,堆可以在執行時動態的擴充套件和收縮。
      • 共享庫:在地址空間的中間附近是一塊用來存放像C標準庫和數學庫這樣共享庫的程式碼和資料的區域。
      • 棧:位於使用者虛擬地址空間頂部的是使用者棧,編譯器用它來實現函式呼叫。和堆一樣每次我們從函式返回時,棧就會收縮。 
      • 核心虛擬儲存器:核心是作業系統總是駐留在儲存器中的部分。地址空間頂部的四分之一部分是為核心預留的。(對使用者的程式來說是禁止訪問的,作業系統的程式碼在此。核心物件也駐留在此)
 
     最容易和上面所謂虛擬地址搞混的一個詞就是“虛擬記憶體”,今天的windows作業系統能夠使得磁碟空間看上去就像記憶體一樣,磁碟上的檔案通常稱為頁檔案(pagefile),從應用程式的角度來看,頁檔案透明地增加了應用程式能夠使用的記憶體的數量(突破實體記憶體大小的限制)。如果計算機擁有1G的RAM(實體記憶體),同時在硬碟上有一個1G的頁檔案,那麼執行的應用程式就認為計算機總共擁有2G的RAM。
實際上並不是真正擁有2GB的RAM(微軟不準備砸記憶體廠商的飯碗)。它的大致原理是將程式在實體記憶體中的各個部分儲存到頁檔案中,當執行的應用程式需要時,再將頁檔案的各個部分重新載入到RAM中。舉例:某程式試圖訪問的資料是在RAM中。在這種情況下,CPU將資料的虛擬地址對映到記憶體的實體地址中,然後執行需要的訪問。執行緒試圖訪問的資料不在RAM中,而是存放在pagefile中的某個地方。這時,試圖訪問就稱為頁錯誤(page fault),CPU將把試圖進行的訪問通知作業系統。這時作業系統就尋找RAM中的一個記憶體空頁。如果找不到空頁,系統必須釋放一個空頁。如果一個頁面尚未被修改,系統就可以釋放該頁面。但是,如果系統需要釋放一個已經修改的頁面,那麼它必須首先將該頁面從RAM拷貝到頁交換檔案中,然後系統進入該頁檔案,找出需要訪問的資料塊,並將資料載入到空閒的記憶體頁面。然後,作業系統更新它的用於指明資料的虛擬記憶體地址現在已經對映到RAM中的相應的物理儲存器地址中的表。這時CPU重新執行生成初始頁面失效的指令,但是這次CPU能夠將虛擬記憶體地址對映到一個物理RAM地址,並訪問該資料塊。
 
     接下來分析一下程式中申請記憶體使用然後釋放(或者不釋放==!)是個什麼情況:
為程式“分配記憶體”,這個概念可以細化:“預定一坨地址空間”,“提交一坨記憶體空間”,“將記憶體空間對映到主存”。而在程式中我們通常所訪問的地址都必須是程式地址空間中被保留和提交的那段地址空間。
     •預定地址空間Reserve:即從程式的4GB地址空間中保留一段地址空間,這個過程通過VirtualAlloc函式完成,並把分配型別引數設定為MEM_RESERVE。這段空間的起始地址必須是系統分配粒度的整數倍,大小必須是系統頁面大小的整數倍。
     •提交記憶體空間Commit:即為程式已保留的地址空間對映機器的記憶體,這裡要特別注意,所謂記憶體一般並不是機器的主存RAM,而只是機器的pagefile。這個過程同樣又VirtualAlloc完成,只是把分配型別引數設定為MEM_COMMIT。這段空間的起始地址和大小都必須是頁面大小的整數倍。這樣程式的對應被提交的區域就被對映到機器的虛擬記憶體上。
     •將記憶體空間對映到主存:這點很重要,作業系統總是隻有在程式提交的頁面被訪問時才將相應的頁面載入到主存中,同時修改程式對應頁面的地址空間對映。這時,程式的地址空間中的對應區域才和機器上的主存對應起來。
 
     解釋了這些終於可以回過頭來看看關於windows記憶體常常提及的幾個指標了:
     Working Set:“Working Set is the current size, in bytes, of the Working Set of this process. The Working Set is the set of memory pages touched recently by the threads in the process. If free memory in the computer is above a threshold, pages are left in the Working Set of a process even if they are not in use. When free memory falls below a threshold, pages are trimmed from Working Sets. If they are needed they will then be soft-faulted back into the Working Set before leaving main memory.”此為官方解釋,實際上該指標記錄了所有對映到程式虛擬地址空間的實體記憶體RAM的大小(即:Task Manager中的Mem Usage),它不僅僅是使用者方式分割槽部分的對映,而是整個程式地址空間的對映。即它同時包括核心方式分割槽中對映到RAM的部分。在使用者方式分割槽部分只有在程式提交的頁面被訪問時才將相應的頁面載入到主存中,而對於該部分的大小總是系統頁面大小的整數倍。隨著程式的不斷執行,影響“Working Set”的因素包括:(1) 機器可用主存的大小 (2) 程式本身“Working Set”的大小範圍。當機器的可用主存小於一定值(闕值)時,系統會釋放一些老的最近沒有被訪問的頁面,把這些頁面通過交換檔案交換到機器的虛擬記憶體中;當Working Set的大小大於該程式所設定的最大值時,同樣會把一些老的頁面交換到機器的虛擬記憶體中。當這些頁面下次再被訪問時,它們才載入到主存。
     Private Bytes:“Private Bytes is the current size, in bytes, of memory that this process has allocated that cannot be shared with other processes.”該指標記錄了程式使用者方式分割槽地址空間中已提交的總的空間大小。無論是直接呼叫API申請的記憶體,被Heap Manager申請的記憶體,或者是CLR 的managed heap,都算在裡面。
     Virtual Bytes:“Virtual Bytes is the current size, in bytes, of the virtual address space the process is using. Use of virtual address space does not necessarily imply corresponding use of either disk or main memory pages. Virtual space is finite, and the process can limit its ability to load libraries.”該指標記錄了當前程式申請成功的其虛擬地址空間的總的空間大小,包括DLL/EXE佔用的地址和通過VirtualAlloc API Reserve(即不管有沒有commit)的Memory Space數量。
     補充一點:如兩個程式都需要同一個DLL的支援,所以在程式執行過程中,這個DLL被對映到了兩個程式的地址空間中,如果這個DLL的大小為4K,在兩個程式中都要提交4K的虛擬地址空間來對映這個DLL。當第一個程式訪問了這個DLL時,這個DLL被載入到機器主存中,這時,第二個程式也要訪問該DLL,這時,系統就不會再載入一遍該DLL了,因為這個DLL已經在主存中了。當然上面所說的訪問僅僅是讀取的操作,如果這時候某個程式要修改DLL對應這段地址中的某個單元時,這時,系統必須為第二個程式分配另外的新頁面,並把要修改位置對應的頁面拷貝的這個新頁面,同時,第二個程式中的這個DLL被對映到這個新頁面上,這就是傳說中的寫時拷貝(Copy on Write)。
 
     其實光是定義了、解釋了這些概念,還是弄不清楚他們分別是對程式執行時哪些具體狀態的寫照、到底什麼指標能夠更準確的描述程式記憶體狀況。
     •Private Bytes are what your app has actually allocated, but include pagefile usage;
     •Working Set is the non-paged Private Bytes plus memory-mapped files;
     •Virtual Bytes are the Working Set plus paged Private Bytes and standby list.
     
     通過上面的描述,首先Working Set不是程式記憶體消耗的全部,該指標是動態的,在測量的過程中會不斷變化。(變化的最小單位為4K)所以Working Set指標強調的是程式對機器主存的消耗,不是程式記憶體的全部資訊。
Private Bytes包含所有為程式提交的記憶體,包括機器主存和虛擬記憶體,可以認為它是程式對實體記憶體消耗,且該指標相對來說更加穩定。在程式產生記憶體洩漏時,該值一定是不斷上漲的。所以一般更傾向於使用Private Bytes來定量程式的記憶體消耗和分析程式的記憶體洩漏。記憶體洩露時表現的現象是私有虛擬記憶體的遞增,而不是工作集大小的遞增。因為在某個點上,記憶體管理器會阻止一個程式繼續增加實體記憶體大小,但它可以繼續增大它的虛擬記憶體大小。
 
     OK,明天還要上班,I need a break … 

相關文章