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