內(nèi)存整理的迷思 看了接二連三出現(xiàn)于本組的有關(guān)內(nèi)存整理的帖子,終于覺得有必要寫一點文字了,這些 帖子如果是在別的組尚且情有可原,可是出現(xiàn)在編程組中卻實屬不該,看來不少人仍然 對Windows的內(nèi)存管理機制存在種種誤解,希望這篇短文能夠澄清這些誤解中的一部分 (如果不是全部的話)。 本文可自由轉(zhuǎn)載,轉(zhuǎn)載不須注明出處,也不須提及作者名字,如要修改內(nèi)容,唯需注意 所述知識之準確性,以免誤人子弟。如發(fā)現(xiàn)本文有錯誤之處,也請不吝指出。 * 進程內(nèi)存布局 Win32中每個進程擁有4GB的虛擬內(nèi)存地址空間。 典型的Winnt系統(tǒng)中的一個進程的內(nèi)存布局如下。 +--------------+ 0xffffffff | 系統(tǒng)代碼 | | 設(shè)備驅(qū)動 | | 內(nèi)存映射文件 | +--------------+ 0x80000000 | 用戶dll映像 | +--------------+ | heap | +--------------+ | stack | +--------------+ | global | +--------------+ | 用戶exe映像 | +--------------+ 0x00010000 | 保留 | +--------------+ 整個4GB虛擬地址空間分為兩部分,上面2GB是系統(tǒng)代碼,下面2GB是用戶代碼(用戶區(qū)最 底部的64KB空間為系統(tǒng)保留), * 物理內(nèi)存分頁 以上是虛擬內(nèi)存,再看物理內(nèi)存。Windows通過2級頁表來將虛擬內(nèi)存地址映射到物理內(nèi) 存。如圖所示: +-----+ +------------------------------------------+ | CR3 | | 一級頁表索引 | 二級頁表索引 | 頁內(nèi)偏移量 | 32位虛擬地址格式 +-----+ +------------------------------------------+ | | | | | | | | | | 第一級頁表 | 第二級頁表 | 物理內(nèi)存 +------+-> +-----------+ +--+-> +-----------+ +-+---> +-----------+ | | 頁表入口 | | | | 頁表入口 | | | | 4KB內(nèi)存頁 | | +-----------+ | | +-----------+ | +---> | | +-> | 頁表入口 +--+ +-> | 頁表入口 +--+ | | +-----------+ +-----------+ +-----------+ | 共1024條 | | 共1024條 | | 4KB內(nèi)存頁 | +-----------+ +-----------+ | | | ... | | ... | | | | | | | +-----------+ | | | | | ... | | | | | | | +-----------+ +-----------+ +-----------+ +------------+----------+ | 20位索引值 | 12位標志 | 頁表入口格式 +------------+----------+ 物理內(nèi)存按4KB為單位劃分為頁面,給定一個32位虛擬地址,Windows首先從CR3寄存器 取得第一級頁表,然后從虛擬地址的一級頁表索引字段取得一級頁表入口,從一級頁表 入口的20位索引可找到對應(yīng)的二級頁表,然后從虛擬地址的二級頁表索引字段取得二級 頁表入口,從二級頁表入口的20位索引找到具體的4KB物理內(nèi)存頁,最后根據(jù)虛擬地址 的頁內(nèi)偏移字段訪問物理內(nèi)存。聽上去比較復(fù)雜,不過對照圖片一看就很清楚了。每個 進程都有自己的一套頁表,對不同的進程,Windows只要在CR3寄存器裝入不同的一級頁 表地址就可以了。(這里給出的是一個概念模型,實際上Windows對頁表訪問還有一些優(yōu) 化技巧) 為什么要分頁呢,這是因為虛擬內(nèi)存中的數(shù)據(jù)不一定必須在物理內(nèi)存中,如果一個頁面 的數(shù)據(jù)在磁盤上,Windows就在對應(yīng)的頁表入口的標志位中做一個標記,這樣訪問到這 個頁面時就引發(fā)一個頁面錯誤。Windows一旦捕捉到頁面錯誤,就將相應(yīng)的頁面從磁盤 載入物理內(nèi)存并再次嘗試讀取,這個過程對應(yīng)用程序來說是透明的,應(yīng)用程序無需關(guān)心 自己要訪問的數(shù)據(jù)是在物理內(nèi)存里還是在磁盤上。 * 內(nèi)存分配 Windows應(yīng)用程序使用VirtualAlloc API函數(shù)分配內(nèi)存塊。也許你用的編程語言使用不 同的關(guān)鍵字,但最終它們都被轉(zhuǎn)換為對VirtualAlloc的調(diào)用。VirtualAlloc分為兩個步 驟,第一步是保留,第二步是提交。保留的意思是將虛擬地址做個標記表示我預(yù)訂了這 個位置,接下來的分配就不會分配在已經(jīng)被預(yù)定的位置了。提交的意思是實際準備開始 用這個內(nèi)存塊。 * 懶惰策略 即使提交了內(nèi)存塊,Windows也并不立即為這段地址初始化頁表。因為可能一段內(nèi)存雖 然被提交,某些區(qū)域卻從來不使用,為這些地址構(gòu)造頁表完全是白費力氣。Windows采 取懶惰策略,一直到某個頁面錯誤出現(xiàn),才為那個頁面創(chuàng)建頁表。這個技術(shù)使得即使分 配很大塊的內(nèi)存也可以在瞬間完成。 * 進程工作集 你可能在想,如果一個進程提交了1GB的虛擬內(nèi)存,并且將這1GB虛擬內(nèi)存全部訪問一遍, 那么是不是它就能占用整個計算機的所有物理內(nèi)存呢?答案是否。 Windows啟動時,根據(jù)計算機上安裝的內(nèi)存數(shù)量計算兩個值“進程默認工作集大小”和 “進程最大工作集大小”。每個進程以默認工作集大小啟動。隨著進程使用內(nèi)存的增加, 工作集可以漸漸增大,直到最大值。如果系統(tǒng)有足夠的空閑頁面,進程工作集甚至可以 超過最大值,反正多出來的內(nèi)存閑著也是閑著。如果系統(tǒng)沒有多余的空閑頁面,而進程 又達到了最大工作集限制,對后續(xù)的頁面錯誤,Windows先刪除該進程的一個頁面,然 后將要求的頁面載入。當空閑內(nèi)存進一步減少時,Windows將開始縮小各個進程的工作 集,將一些頁面換出內(nèi)存。 所以,一個惡意的或錯誤的程序?qū)嶋H上并沒有辦法用拼命分配內(nèi)存的方法對系統(tǒng)造成過 大的影響。 * 內(nèi)存整理 有了上面這些知識,你就很容易看出來所謂的內(nèi)存整理有多么荒謬。物理內(nèi)存按4KB分 頁,根本無所謂碎片化,就算物理內(nèi)存堆放得再整齊連續(xù),系統(tǒng)總是按照4KB為單位訪 問它。 我所見的大多數(shù)內(nèi)存整理程序的做法是分配一塊很大的內(nèi)存,意圖將其他進程的數(shù)據(jù)換 入磁盤,然后釋放這塊內(nèi)存來得到大塊物理內(nèi)存。然而由于Windows的工作集裁剪策略, 這個做法實際上無法起作用,如果系統(tǒng)的內(nèi)存壓力相當重,那么不管這個程序試圖分配 多少內(nèi)存,結(jié)果只是導(dǎo)致自己的內(nèi)存被換出,而不是其他進程的。 退一步說,即使這個動作能夠起到將其他進程的內(nèi)存換出的作用,但這實際上只是一個 損害系統(tǒng)性能的動作,而不是一種優(yōu)化,因為很快其他進程就會產(chǎn)生大量頁面錯誤,結(jié) 果就是硬盤猛轉(zhuǎn)。 * 堆碎片化問題 整理物理內(nèi)存雖然是無稽之談,但進程的動態(tài)存儲區(qū)--heap,確實是會有碎片化問題的。 準確地說,這不是內(nèi)存碎片,而是地址碎片。如果程序反復(fù)分配釋放小塊內(nèi)存,heap的 地址可能變得很不連續(xù),雖然耗盡2GB虛擬地址的可能不大,但在碎片化的堆中尋找一 塊可用內(nèi)存就會變得比較慢從而影響執(zhí)行效率。 解決這個問題的方法只能是寫程序的時候注意考慮這個問題,而不可能借助外部程序。 例如使用一個內(nèi)存池來管理自己的內(nèi)存,Jeffrey Richter的 <Advanced Windows> 一書 中介紹了一種重載class的operator new的方法。 -- Felix |
|