概述
我們都知道一個進程是與其他進程共享CPU和內(nèi)存資源的。正因如此赶促,操作系統(tǒng)需要有一套完善的內(nèi)存管理機制才能防止進程之間內(nèi)存泄漏的問題。
為了更加有效地管理內(nèi)存并減少出錯,現(xiàn)代操作系統(tǒng)提供了一種對主存的抽象概念蘸劈,即是虛擬內(nèi)存(Virtual Memory)。虛擬內(nèi)存為每個進程提供了一個一致的尊沸、私有的地址空間威沫,它讓每個進程產(chǎn)生了一種自己在獨享主存的錯覺(每個進程擁有一片連續(xù)完整的內(nèi)存空間)。
理解不深刻的人會認為虛擬內(nèi)存只是“使用硬盤空間來擴展內(nèi)存“的技術(shù)洼专,這是不對的棒掠。虛擬內(nèi)存的重要意義是它定義了一個連續(xù)的虛擬地址空間,使得程序的編寫難度降低屁商。并且烟很,把內(nèi)存擴展到硬盤空間只是使用虛擬內(nèi)存的必然結(jié)果,虛擬內(nèi)存空間會存在硬盤中,并且會被內(nèi)存緩存(按需)雾袱,有的操作系統(tǒng)還會在內(nèi)存不夠的情況下恤筛,將某一進程的內(nèi)存全部放入硬盤空間中,并在切換到該進程時再從硬盤讀取(這也是為什么Windows會經(jīng)常假死的原因...)芹橡。
虛擬內(nèi)存主要提供了如下三個重要的能力:
它把主存看作為一個存儲在硬盤上的虛擬地址空間的高速緩存毒坛,并且只在主存中緩存活動區(qū)域(按需緩存)。
它為每個進程提供了一個一致的地址空間林说,從而降低了程序員對內(nèi)存管理的復(fù)雜性煎殷。
它還保護了每個進程的地址空間不會被其他進程破壞。
介紹了虛擬內(nèi)存的基本概念之后腿箩,接下來的內(nèi)容將會從虛擬內(nèi)存在硬件中如何運作逐漸過渡到虛擬內(nèi)存在操作系統(tǒng)(Linux)中的實現(xiàn)豪直。
CPU尋址
內(nèi)存通常被組織為一個由M個連續(xù)的字節(jié)大小的單元組成的數(shù)組,每個字節(jié)都有一個唯一的物理地址(Physical Address PA)珠移,作為到數(shù)組的索引顶伞。CPU訪問內(nèi)存最簡單直接的方法就是使用物理地址,這種尋址方式被稱為物理尋址剑梳。
現(xiàn)代處理器使用的是一種稱為虛擬尋址(Virtual Addressing)的尋址方式唆貌。使用虛擬尋址,CPU需要將虛擬地址翻譯成物理地址垢乙,這樣才能訪問到真實的物理內(nèi)存锨咙。
虛擬尋址需要硬件與操作系統(tǒng)之間互相合作。CPU中含有一個被稱為內(nèi)存管理單元(Memory Management Unit, MMU)的硬件追逮,它的功能是將虛擬地址轉(zhuǎn)換為物理地址酪刀。MMU需要借助存放在內(nèi)存中的頁表來動態(tài)翻譯虛擬地址,該頁表由操作系統(tǒng)管理钮孵。
頁表
虛擬內(nèi)存空間被組織為一個存放在硬盤上的M個連續(xù)的字節(jié)大小的單元組成的數(shù)組骂倘,每個字節(jié)都有一個唯一的虛擬地址,作為到數(shù)組的索引(這點其實與物理內(nèi)存是一樣的)巴席。
操作系統(tǒng)通過將虛擬內(nèi)存分割為大小固定的塊來作為硬盤和內(nèi)存之間的傳輸單位历涝,這個塊被稱為虛擬頁(Virtual Page, VP),每個虛擬頁的大小為P=2^p
字節(jié)漾唉。物理內(nèi)存也會按照這種方法分割為物理頁(Physical Page, PP)荧库,大小也為P
字節(jié)。
CPU在獲得虛擬地址之后赵刑,需要通過MMU將虛擬地址翻譯為物理地址分衫。而在翻譯的過程中還需要借助頁表,所謂頁表就是一個存放在物理內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)般此,它記錄了虛擬頁與物理頁的映射關(guān)系蚪战。
頁表是一個元素為頁表條目(Page Table Entry, PTE)的集合牵现,每個虛擬頁在頁表中一個固定偏移量的位置上都有一個PTE。下面是PTE僅含有一個有效位標(biāo)記的頁表結(jié)構(gòu)邀桑,該有效位代表這個虛擬頁是否被緩存在物理內(nèi)存中施籍。
虛擬頁VP 0
、VP 4
概漱、VP 6
丑慎、VP 7
被緩存在物理內(nèi)存中,虛擬頁VP 2
和VP 5
被分配在頁表中瓤摧,但并沒有緩存在物理內(nèi)存竿裂,虛擬頁VP 1
和VP 3
還沒有被分配。
在進行動態(tài)內(nèi)存分配時照弥,例如malloc()
函數(shù)或者其他高級語言中的new
關(guān)鍵字腻异,操作系統(tǒng)會在硬盤中創(chuàng)建或申請一段虛擬內(nèi)存空間,并更新到頁表(分配一個PTE这揣,使該PTE指向硬盤上這個新創(chuàng)建的虛擬頁)悔常。
由于CPU每次進行地址翻譯的時候都需要經(jīng)過PTE,所以如果想控制內(nèi)存系統(tǒng)的訪問给赞,可以在PTE上添加一些額外的許可位(例如讀寫權(quán)限机打、內(nèi)核權(quán)限等),這樣只要有指令違反了這些許可條件片迅,CPU就會觸發(fā)一個一般保護故障残邀,將控制權(quán)傳遞給內(nèi)核中的異常處理程序。一般這種異常被稱為“段錯誤(Segmentation Fault)”柑蛇。
頁命中
如上圖所示芥挣,MMU根據(jù)虛擬地址在頁表中尋址到了PTE 4
,該PTE的有效位為1耻台,代表該虛擬頁已經(jīng)被緩存在物理內(nèi)存中了空免,最終MMU得到了PTE中的物理內(nèi)存地址(指向PP 1
)。
缺頁
如上圖所示盆耽,MMU根據(jù)虛擬地址在頁表中尋址到了PTE 2
蹋砚,該PTE的有效位為0,代表該虛擬頁并沒有被緩存在物理內(nèi)存中征字。虛擬頁沒有被緩存在物理內(nèi)存中(緩存未命中)被稱為缺頁都弹。
當(dāng)CPU遇見缺頁時會觸發(fā)一個缺頁異常娇豫,缺頁異常將控制權(quán)轉(zhuǎn)向操作系統(tǒng)內(nèi)核街望,然后調(diào)用內(nèi)核中的缺頁異常處理程序封拧,該程序會選擇一個犧牲頁,如果犧牲頁已被修改過妈拌,內(nèi)核會先將它復(fù)制回硬盤(采用寫回機制而不是直寫也是為了盡量減少對硬盤的訪問次數(shù)),然后再把該虛擬頁覆蓋到犧牲頁的位置艰匙,并且更新PTE。
當(dāng)缺頁異常處理程序返回時,它會重新啟動導(dǎo)致缺頁的指令咪辱,該指令會把導(dǎo)致缺頁的虛擬地址重新發(fā)送給MMU。由于現(xiàn)在已經(jīng)成功處理了缺頁異常椎组,所以最終結(jié)果是頁命中油狂,并得到物理地址。
這種在硬盤和內(nèi)存之間傳送頁的行為稱為頁面調(diào)度(paging):頁從硬盤換入內(nèi)存和從內(nèi)存換出到硬盤寸癌。當(dāng)缺頁異常發(fā)生時专筷,才將頁面換入到內(nèi)存的策略稱為按需頁面調(diào)度(demand paging),所有現(xiàn)代操作系統(tǒng)基本都使用的是按需頁面調(diào)度的策略蒸苇。
虛擬內(nèi)存跟CPU高速緩存(或其他使用緩存的技術(shù))一樣依賴于局部性原則磷蛹。雖然處理缺頁消耗的性能很多(畢竟還是要從硬盤中讀取)溪烤,而且程序在運行過程中引用的不同虛擬頁的總數(shù)可能會超出物理內(nèi)存的大小味咳,但是局部性原則保證了在任意時刻,程序?qū)②呄蛴谠谝粋€較小的活動頁面(active page)集合上工作檬嘀,這個集合被稱為工作集(working set)槽驶。根據(jù)空間局部性原則(一個被訪問過的內(nèi)存地址以及其周邊的內(nèi)存地址都會有很大幾率被再次訪問)與時間局部性原則(一個被訪問過的內(nèi)存地址在之后會有很大幾率被再次訪問),只要將工作集緩存在物理內(nèi)存中鸳兽,接下來的地址翻譯請求很大幾率都在其中捺檬,從而減少了額外的硬盤流量。
如果一個程序沒有良好的局部性贸铜,將會使工作集的大小不斷膨脹堡纬,直至超過物理內(nèi)存的大小,這時程序會產(chǎn)生一種叫做抖動(thrashing)的狀態(tài)蒿秦,頁面會不斷地換入換出烤镐,如此多次的讀寫硬盤開銷,性能自然會十分“恐怖”棍鳖。所以炮叶,想要編寫出性能高效的程序,首先要保證程序的時間局部性與空間局部性渡处。
多級頁表
我們目前為止討論的只是單頁表镜悉,但在實際的環(huán)境中虛擬空間地址都是很大的(一個32位系統(tǒng)的地址空間有2^32 = 4GB
,更別說64位系統(tǒng)了)医瘫。在這種情況下侣肄,使用一個單頁表明顯是效率低下的。
常用方法是使用層次結(jié)構(gòu)的頁表醇份。假設(shè)我們的環(huán)境為一個32位的虛擬地址空間稼锅,它有如下形式:
虛擬地址空間被分為4KB的頁吼具,每個PTE都是4字節(jié)。
內(nèi)存的前2K個頁面分配給了代碼和數(shù)據(jù)矩距。
之后的6K個頁面還未被分配拗盒。
再接下來的1023個頁面也未分配,其后的1個頁面分配給了用戶棧锥债。
下圖是為該虛擬地址空間構(gòu)造的二級頁表層次結(jié)構(gòu)(真實情況中多為四級或更多)陡蝇,一級頁表(1024個PTE正好覆蓋4GB的虛擬地址空間,同時每個PTE只有4字節(jié)哮肚,這樣一級頁表與二級頁表的大小也正好與一個頁面的大小一致都為4KB)的每個PTE負責(zé)映射虛擬地址空間中一個4MB的片(chunk)毅整,每一片都由1024個連續(xù)的頁面組成。二級頁表中的每個PTE負責(zé)映射一個4KB的虛擬內(nèi)存頁面绽左。
這個結(jié)構(gòu)看起來很像是一個B-Tree
悼嫉,這種層次結(jié)構(gòu)有效的減緩了內(nèi)存要求:
如果一個一級頁表的一個PTE是空的,那么相應(yīng)的二級頁表也不會存在拼窥。這代表一種巨大的潛在節(jié)約(對于一個普通的程序來說戏蔑,虛擬地址空間的大部分都會是未分配的)。
只有一級頁表才總是需要緩存在內(nèi)存中的鲁纠,這樣虛擬內(nèi)存系統(tǒng)就可以在需要時創(chuàng)建总棵、頁面調(diào)入或調(diào)出二級頁表(只有經(jīng)常使用的二級頁表才會被緩存在內(nèi)存中),這就減少了內(nèi)存的壓力改含。
地址翻譯的過程
從形式上來說情龄,地址翻譯是一個N元素的虛擬地址空間中的元素和一個M元素的物理地址空間中元素之間的映射。
下圖為MMU利用頁表進行尋址的過程:
頁表基址寄存器(PTBR)指向當(dāng)前頁表捍壤。一個n位的虛擬地址包含兩個部分骤视,一個p位的虛擬頁面偏移量(Virtual Page Offset, VPO)和一個(n - p)位的虛擬頁號(Virtual Page Number, VPN)。
MMU根據(jù)VPN來選擇對應(yīng)的PTE鹃觉,例如VPN 0
代表PTE 0
专酗、VPN 1
代表PTE 1
....因為物理頁與虛擬頁的大小是一致的,所以物理頁面偏移量(Physical Page Offset, PPO)與VPO是相同的盗扇。那么之后只要將PTE中的物理頁號(Physical Page Number, PPN)與虛擬地址中的VPO串聯(lián)起來祷肯,就能得到相應(yīng)的物理地址。
多級頁表的地址翻譯也是如此疗隶,只不過因為有多個層次佑笋,所以VPN需要分成多段。假設(shè)有一個k級頁表斑鼻,虛擬地址會被分割成k個VPN和1個VPO蒋纬,每個VPN i
都是一個到第i級頁表的索引。為了構(gòu)造物理地址,MMU需要訪問k個PTE才能拿到對應(yīng)的PPN颠锉。
TLB
頁表是被緩存在內(nèi)存中的法牲,盡管內(nèi)存的速度相對于硬盤來說已經(jīng)非呈泛梗快了琼掠,但與CPU還是有所差距。為了防止每次地址翻譯操作都需要去訪問內(nèi)存停撞,CPU使用了高速緩存與TLB來緩存PTE瓷蛙。
在最糟糕的情況下(不包括缺頁),MMU需要訪問內(nèi)存取得相應(yīng)的PTE戈毒,這個代價大約為幾十到幾百個周期艰猬,如果PTE湊巧緩存在L1高速緩存中(如果L1沒有還會從L2中查找,不過我們忽略多級緩沖區(qū)的細節(jié))埋市,那么性能開銷就會下降到1個或2個周期冠桃。然而,許多系統(tǒng)甚至需要消除即使這樣微小的開銷道宅,TLB由此而生食听。
TLB(Translation Lookaside Buffer, TLB)被稱為翻譯后備緩沖器或翻譯旁路緩沖器,它是MMU中的一個緩沖區(qū)污茵,其中每一行都保存著一個由單個PTE組成的塊樱报。用于組選擇和行匹配的索引與標(biāo)記字段是從VPN中提取出來的,如果TLB中有T = 2^t
個組泞当,那么TLB索引(TLBI)是由VPN的t個最低位組成的迹蛤,而TLB標(biāo)記(TLBT)是由VPN中剩余的位組成的。
下圖為地址翻譯的流程(TLB命中的情況下):
第一步襟士,CPU將一個虛擬地址交給MMU進行地址翻譯盗飒。
第二步和第三步,MMU通過TLB取得相應(yīng)的PTE陋桂。
第四步箩兽,MMU通過PTE翻譯出物理地址并將它發(fā)送給高速緩存/內(nèi)存。
第五步章喉,高速緩存返回數(shù)據(jù)到CPU(如果緩存命中的話汗贫,否則還需要訪問內(nèi)存)。
當(dāng)TLB未命中時秸脱,MMU必須從高速緩存/內(nèi)存中取出相應(yīng)的PTE落包,并將新取得的PTE存放到TLB(如果TLB已滿會覆蓋一個已經(jīng)存在的PTE)。
Linux中的虛擬內(nèi)存系統(tǒng)
Linux為每個進程維護了一個單獨的虛擬地址空間摊唇。虛擬地址空間分為內(nèi)核空間與用戶空間咐蝇,用戶空間包括代碼、數(shù)據(jù)巷查、堆有序、共享庫以及棧抹腿,內(nèi)核空間包括內(nèi)核中的代碼和數(shù)據(jù)結(jié)構(gòu),內(nèi)核空間的某些區(qū)域被映射到所有進程共享的物理頁面旭寿。Linux也將一組連續(xù)的虛擬頁面(大小等于內(nèi)存總量)映射到相應(yīng)的一組連續(xù)的物理頁面警绩,這種做法為內(nèi)核提供了一種便利的方法來訪問物理內(nèi)存中任何特定的位置。
Linux將虛擬內(nèi)存組織成一些區(qū)域(也稱為段)的集合盅称,區(qū)域的概念允許虛擬地址空間有間隙肩祥。一個區(qū)域就是已經(jīng)存在著的已分配的虛擬內(nèi)存的連續(xù)片(chunk)。例如缩膝,代碼段将饺、數(shù)據(jù)段予弧、堆桌肴、共享庫段坠七,以及用戶棧都屬于不同的區(qū)域,每個存在的虛擬頁都保存在某個區(qū)域中拳魁,而不屬于任何區(qū)域的虛擬頁是不存在的潘懊,也不能被進程所引用授舟。
內(nèi)核為系統(tǒng)中的每個進程維護一個單獨的任務(wù)結(jié)構(gòu)(task_struct)。任務(wù)結(jié)構(gòu)中的元素包含或者指向內(nèi)核運行該進程所需的所有信息(PID奢啥、指向用戶棧的指針寂纪、可執(zhí)行目標(biāo)文件的名字弊攘、程序計數(shù)器等)。
mm_struct:描述了虛擬內(nèi)存的當(dāng)前狀態(tài)。pgd指向一級頁表的基址(當(dāng)內(nèi)核運行這個進程時宴合,pgd會被存放在CR3控制寄存器卦洽,也就是頁表基址寄存器中),mmap指向一個vm_area_structs的鏈表蚤霞,其中每個vm_area_structs都描述了當(dāng)前虛擬地址空間的一個區(qū)域昧绣。
vm_starts:指向這個區(qū)域的起始處。
vm_end:指向這個區(qū)域的結(jié)束處删壮。
vm_prot:描述這個區(qū)域內(nèi)包含的所有頁的讀寫許可權(quán)限兔簇。
vm_flags:描述這個區(qū)域內(nèi)的頁面是與其他進程共享的垄琐,還是這個進程私有的以及一些其他信息墩朦。
vm_next:指向鏈表的下一個區(qū)域結(jié)構(gòu)氓涣。
內(nèi)存映射
Linux通過將一個虛擬內(nèi)存區(qū)域與一個硬盤上的文件關(guān)聯(lián)起來,以初始化這個虛擬內(nèi)存區(qū)域的內(nèi)容痒玩,這個過程稱為內(nèi)存映射(memory mapping)。這種將虛擬內(nèi)存系統(tǒng)集成到文件系統(tǒng)的方法可以簡單而高效地把程序和數(shù)據(jù)加載到內(nèi)存中草讶。
一個區(qū)域可以映射到一個普通硬盤文件的連續(xù)部分堕战,例如一個可執(zhí)行目標(biāo)文件。文件區(qū)(section)被分成頁大小的片屿讽,每一片包含一個虛擬頁的初始內(nèi)容。由于按需頁面調(diào)度的策略诵棵,這些虛擬頁面沒有實際交換進入物理內(nèi)存嘶窄,直到CPU引用的虛擬地址在該區(qū)域的范圍內(nèi)柄冲。如果區(qū)域比文件區(qū)要大,那么就用零來填充這個區(qū)域的余下部分戒祠。
一個區(qū)域也可以映射到一個匿名文件姜盈,匿名文件是由內(nèi)核創(chuàng)建的贩据,包含的全是二進制零矾芙。當(dāng)CPU第一次引用這樣一個區(qū)域內(nèi)的虛擬頁面時拂铡,內(nèi)核就在物理內(nèi)存中找到一個合適的犧牲頁面,如果該頁面被修改過失球,就先將它寫回到硬盤,之后用二進制零覆蓋犧牲頁并更新頁表烈疚,將這個頁面標(biāo)記為已緩存在內(nèi)存中的黔牵。
簡單的來說:普通文件映射就是將一個文件與一塊內(nèi)存建立起映射關(guān)系,對該文件進行IO操作可以繞過內(nèi)核直接在用戶態(tài)完成(用戶態(tài)在該虛擬地址區(qū)域讀寫就相當(dāng)于讀寫這個文件)爷肝。匿名文件映射一般在用戶空間需要分配一段內(nèi)存來存放數(shù)據(jù)時猾浦,由內(nèi)核創(chuàng)建匿名文件并與內(nèi)存進行映射陆错,之后用戶態(tài)就可以通過操作這段虛擬地址來操作內(nèi)存了。匿名文件映射最熟悉的應(yīng)用場景就是動態(tài)內(nèi)存分配(malloc()函數(shù))金赦。
Linux很多地方都采用了“懶加載”機制危号,自然也包括內(nèi)存映射。不管是普通文件映射還是匿名映射摆舟,Linux只會先劃分虛擬內(nèi)存地址蛇受。只有當(dāng)CPU第一次訪問該區(qū)域內(nèi)的虛擬地址時忆矛,才會真正的與物理內(nèi)存建立映射關(guān)系。
只要虛擬頁被初始化了,它就在一個由內(nèi)核維護的交換文件(swap file)之間換來換去诡挂。交換文件又稱為交換空間(swap space)或交換區(qū)域(swap area)。swap區(qū)域不止用于頁交換间聊,在物理內(nèi)存不夠的情況下偷遗,還會將部分內(nèi)存數(shù)據(jù)交換到swap區(qū)域(使用硬盤來擴展內(nèi)存)纪铺。
共享對象
虛擬內(nèi)存系統(tǒng)為每個進程提供了私有的虛擬地址空間,這樣可以保證進程之間不會發(fā)生錯誤的讀寫周拐。但多個進程之間也含有相同的部分露泊,例如每個C程序都使用到了C標(biāo)準(zhǔn)庫,如果每個進程都在物理內(nèi)存中保持這些代碼的副本畜眨,那會造成很大的內(nèi)存資源浪費胞四。
內(nèi)存映射提供了共享對象的機制撬讽,來避免內(nèi)存資源的浪費囚聚。一個對象被映射到虛擬內(nèi)存的一個區(qū)域竣贪,要么是作為共享對象淑际,要么是作為私有對象的。
如果一個進程將一個共享對象映射到它的虛擬地址空間的一個區(qū)域內(nèi)扇住,那么這個進程對這個區(qū)域的任何寫操作春缕,對于那些也把這個共享對象映射到它們虛擬內(nèi)存的其他進程而言,也是可見的艘蹋。相對的锄贼,對一個映射到私有對象的區(qū)域的任何寫操作,對于其他進程來說是不可見的女阀。一個映射到共享對象的虛擬內(nèi)存區(qū)域叫做共享區(qū)域宅荤,類似地屑迂,也有私有區(qū)域。
為了節(jié)約內(nèi)存冯键,私有對象開始的生命周期與共享對象基本上是一致的(在物理內(nèi)存中只保存私有對象的一份副本)惹盼,并使用寫時復(fù)制的技術(shù)來應(yīng)對多個進程的寫沖突。
只要沒有進程試圖寫它自己的私有區(qū)域惫确,那么多個進程就可以繼續(xù)共享物理內(nèi)存中私有對象的一個單獨副本手报。然而,只要有一個進程試圖對私有區(qū)域的某一頁面進行寫操作改化,就會觸發(fā)一個保護異常掩蛤。在上圖中,進程B試圖對私有區(qū)域的一個頁面進行寫操作陈肛,該操作觸發(fā)了保護異常揍鸟。異常處理程序會在物理內(nèi)存中創(chuàng)建這個頁面的一個新副本,并更新PTE指向這個新的副本句旱,然后恢復(fù)這個頁的可寫權(quán)限阳藻。
還有一個典型的例子就是fork()
函數(shù),該函數(shù)用于創(chuàng)建子進程前翎。當(dāng)fork()
函數(shù)被當(dāng)前進程調(diào)用時稚配,內(nèi)核會為新進程創(chuàng)建各種必要的數(shù)據(jù)結(jié)構(gòu),并分配給它一個唯一的PID港华。為了給新進程創(chuàng)建虛擬內(nèi)存道川,它復(fù)制了當(dāng)前進程的mm_struct
、vm_area_struct
和頁表的原樣副本立宜。并將兩個進程的每個頁面都標(biāo)為只讀冒萄,兩個進程中的每個區(qū)域都標(biāo)記為私有區(qū)域(寫時復(fù)制)。
這樣橙数,父進程和子進程的虛擬內(nèi)存空間完全一致尊流,只有當(dāng)這兩個進程中的任一個進行寫操作時,再使用寫時復(fù)制來保證每個進程的虛擬地址空間私有的抽象概念灯帮。
動態(tài)內(nèi)存分配
雖然可以使用內(nèi)存映射(mmap()
函數(shù))來創(chuàng)建和刪除虛擬內(nèi)存區(qū)域來滿足運行時動態(tài)內(nèi)存分配的問題崖技。然而,為了更好的移植性與便利性钟哥,還需要一個更高層面的抽象迎献,也就是動態(tài)內(nèi)存分配器(dynamic memory allocator)。
動態(tài)內(nèi)存分配器維護著一個進程的虛擬內(nèi)存區(qū)域腻贰,也就是我們所熟悉的“堆(heap)”吁恍,內(nèi)核中還維護著一個指向堆頂?shù)闹羔榖rk(break)。動態(tài)內(nèi)存分配器將堆視為一個連續(xù)的虛擬內(nèi)存塊(chunk)的集合,每個塊有兩種狀態(tài)冀瓦,已分配和空閑伴奥。已分配的塊顯式地保留為供應(yīng)用程序使用,空閑塊則可以用來進行分配翼闽,它的空閑狀態(tài)直到它顯式地被應(yīng)用程序分配為止拾徙。已分配的塊要么被應(yīng)用程序顯式釋放,要么被垃圾回收器所釋放肄程。
本文只講解動態(tài)內(nèi)存分配的一些概念锣吼,關(guān)于動態(tài)內(nèi)存分配器的實現(xiàn)已經(jīng)超出了本文的討論范圍。如果有對它感興趣的同學(xué)蓝厌,可以去參考dlmalloc的源碼玄叠,它是由Doug Lea(就是寫Java并發(fā)包的那位)實現(xiàn)的一個設(shè)計巧妙的內(nèi)存分配器,而且源碼中的注釋十分多拓提。
內(nèi)存碎片
造成堆的空間利用率很低的主要原因是一種被稱為碎片(fragmentation)的現(xiàn)象读恃,當(dāng)雖然有未使用的內(nèi)存但這塊內(nèi)存并不能滿足分配請求時,就會產(chǎn)生碎片代态。有以下兩種形式的碎片:
內(nèi)部碎片:在一個已分配塊比有效載荷大時發(fā)生寺惫。例如,程序請求一個5字(這里我們不糾結(jié)字的大小蹦疑,假設(shè)一個字為4字節(jié)西雀,堆的大小為16字并且要保證邊界雙字對齊)的塊,內(nèi)存分配器為了保證空閑塊是雙字邊界對齊的(具體實現(xiàn)中對齊的規(guī)定可能略有不同歉摧,但對齊是肯定會有的)艇肴,只好分配一個6字的塊。在本例中叁温,已分配塊為6字再悼,有效載荷為5字,內(nèi)部碎片為已分配塊減去有效載荷膝但,為1字冲九。
外部碎片:當(dāng)空閑內(nèi)存合計起來足夠滿足一個分配請求,但是沒有一個單獨的空閑塊足夠大到可以來處理這個請求時發(fā)生跟束。外部碎片難以量化且不可預(yù)測莺奸,所以分配器通常采用啟發(fā)式策略來試圖維持少量的大空閑塊,而不是維持大量的小空閑塊冀宴。分配器也會根據(jù)策略與分配請求的匹配來分割空閑塊與合并空閑塊(必須相鄰)憾筏。
空閑鏈表
分配器將堆組織為一個連續(xù)的已分配塊和空閑塊的序列,該序列被稱為空閑鏈表花鹅。空閑鏈表分為隱式空閑鏈表與顯式空閑鏈表枫浙。
隱式空閑鏈表刨肃,是一個單向鏈表古拴,并且每個空閑塊僅僅是通過頭部中的大小字段隱含地連接著的。
顯式空閑鏈表真友,即是將空閑塊組織為某種形式的顯式數(shù)據(jù)結(jié)構(gòu)(為了更加高效地合并與分割空閑塊)黄痪。例如,將堆組織為一個雙向空閑鏈表盔然,在每個空閑塊中桅打,都包含一個前驅(qū)節(jié)點的指針與后繼節(jié)點的指針。
查找一個空閑塊一般有如下幾種策略:
首次適配:從頭開始搜索空閑鏈表愈案,選擇第一個遇見的合適的空閑塊挺尾。它的優(yōu)點在于趨向于將大的空閑塊保留在鏈表的后面,缺點是它趨向于在靠近鏈表前部處留下碎片站绪。
下一次適配:每次從上一次查詢結(jié)束的地方開始進行搜索遭铺,直到遇見合適的空閑塊。這種策略通常比首次適配效率高恢准,但是內(nèi)存利用率則要低得多了魂挂。
最佳適配:檢查每個空閑塊,選擇適合所需請求大小的最小空閑塊馁筐。最佳適配的內(nèi)存利用率是三種策略中最高的涂召,但它需要對堆進行徹底的搜索。
對一個鏈表進行查找操作的效率是線性的敏沉,為了減少分配請求對空閑塊匹配的時間果正,分配器通常采用分離存儲(segregated storage)的策略,即是維護多個空閑鏈表赦抖,其中每個鏈表的塊有大致相等的大小舱卡。
一種簡單的分離存儲策略:分配器維護一個空閑鏈表數(shù)組,然后將所有可能的塊分成一些等價類(也叫做大小類(size class))队萤,每個大小類代表一個空閑鏈表轮锥,并且每個大小類的空閑鏈表包含大小相等的塊,每個塊的大小就是這個大小類中最大元素的大幸(例如舍杜,某個大小類的范圍定義為(17~32),那么這個空閑鏈表全由大小為32的塊組成)赵辕。
當(dāng)有一個分配請求時既绩,我們檢查相應(yīng)的空閑鏈表。如果鏈表非空还惠,那么就分配其中第一塊的全部饲握。如果鏈表為空,分配器就向操作系統(tǒng)請求一個固定大小的額外內(nèi)存片,將這個片分成大小相等的塊救欧,然后將這些塊鏈接起來形成新的空閑鏈表衰粹。
要釋放一個塊,分配器只需要簡單地將這個塊插入到相應(yīng)的空閑鏈表的頭部笆怠。
垃圾回收
在編寫C程序時铝耻,一般只能顯式地分配與釋放堆中的內(nèi)存(malloc()
與free()
),程序員不僅需要分配內(nèi)存蹬刷,還需要負責(zé)內(nèi)存的釋放瓢捉。
許多現(xiàn)代編程語言都內(nèi)置了自動內(nèi)存管理機制(通過引入自動內(nèi)存管理庫也可以讓C/C++實現(xiàn)自動內(nèi)存管理),所謂自動內(nèi)存管理办成,就是自動判斷不再需要的堆內(nèi)存(被稱為垃圾內(nèi)存)泡态,然后自動釋放這些垃圾內(nèi)存。
自動內(nèi)存管理的實現(xiàn)是垃圾收集器(garbage collector)诈火,它是一種動態(tài)內(nèi)存分配器兽赁,它會自動釋放應(yīng)用程序不再需要的已分配塊。
垃圾收集器一般采用以下兩種(之一)的策略來判斷一塊堆內(nèi)存是否為垃圾內(nèi)存:
引用計數(shù)器:在數(shù)據(jù)的物理空間中添加一個計數(shù)器冷守,當(dāng)有其他數(shù)據(jù)與其相關(guān)時(引用)刀崖,該計數(shù)器加一,反之則減一拍摇。通過定期檢查計數(shù)器的值亮钦,只要為0則認為是垃圾內(nèi)存,可以釋放它所占用的已分配塊充活。使用引用計數(shù)器蜂莉,實現(xiàn)簡單直接,但缺點也很明顯混卵,它無法回收循環(huán)引用的兩個對象(假設(shè)有對象A與對象B映穗,它們2個互相引用,但實際上對象A與對象B都已經(jīng)是沒用的對象了)幕随。
可達性分析:垃圾收集器將堆內(nèi)存視為一張有向圖蚁滋,然后選出一組根節(jié)點(例如,在Java中一般為類加載器赘淮、全局變量辕录、運行時常量池中的引用類型變量等),根節(jié)點必須是足夠“活躍“的對象梢卸。然后計算從根節(jié)點集合出發(fā)的可達路徑走诞,只要從根節(jié)點出發(fā)不可達的節(jié)點,都視為垃圾內(nèi)存蛤高。
垃圾收集器進行回收的算法有如下幾種:
標(biāo)記-清除:該算法分為標(biāo)記(mark)和清除(sweep)兩個階段蚣旱。首先標(biāo)記出所有需要回收的對象碑幅,然后在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象。標(biāo)記-清除算法實現(xiàn)簡單姻锁,但它的效率不高枕赵,而且會產(chǎn)生許多內(nèi)存碎片。
標(biāo)記-整理:標(biāo)記-整理與標(biāo)記-清除算法基本一致位隶,只不過后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動开皿,然后直接清理掉邊界以外的內(nèi)存涧黄。
復(fù)制:將程序所擁有的內(nèi)存空間劃分為大小相等的兩塊,每次都只使用其中的一塊赋荆。當(dāng)這一塊的內(nèi)存用完了笋妥,就把還存活著的對象復(fù)制到另一塊內(nèi)存上,然后將已使用過的內(nèi)存空間進行清理窄潭。這種方法不必考慮內(nèi)存碎片問題春宣,但內(nèi)存利用率很低。這個比例不是絕對的嫉你,像HotSpot虛擬機為了避免浪費月帝,將內(nèi)存劃分為Eden空間與兩個Survivor空間,每次都只使用Eden和其中一個Survivor幽污。當(dāng)回收時嚷辅,將Eden和Survivor中還存活著的對象一次性地復(fù)制到另外一個Survivor空間上,然后清理掉Eden和剛才使用過的Survivor空間距误。HotSpot虛擬機默認的Eden和Survivor的大小比例為8:1簸搞,只有10%的內(nèi)存空間會被閑置浪費。
分代:分代算法根據(jù)對象的存活周期的不同將內(nèi)存劃分為多塊准潭,這樣就可以對不同的年代采用不同的回收算法趁俊。一般分為新生代與老年代,新生代存放的是存活率較低的對象刑然,可以采用復(fù)制算法寺擂;老年代存放的是存活率較高的對象,如果使用復(fù)制算法闰集,那么內(nèi)存空間會不夠用沽讹,所以必須使用標(biāo)記-清除或標(biāo)記-整理算法。
總結(jié)
虛擬內(nèi)存是對內(nèi)存的一個抽象武鲁。支持虛擬內(nèi)存的CPU需要通過虛擬尋址的方式來引用內(nèi)存中的數(shù)據(jù)爽雄。CPU加載一個虛擬地址,然后發(fā)送給MMU進行地址翻譯沐鼠。地址翻譯需要硬件與操作系統(tǒng)之間緊密合作挚瘟,MMU借助頁表來獲得物理地址叹谁。
首先,MMU先將虛擬地址發(fā)送給TLB以獲得PTE(根據(jù)VPN尋址)乘盖。
如果恰好TLB中緩存了該PTE焰檩,那么就返回給MMU,否則MMU需要從高速緩存/內(nèi)存中獲得PTE订框,然后更新緩存到TLB析苫。
MMU獲得了PTE,就可以從PTE中獲得對應(yīng)的PPN穿扳,然后結(jié)合VPO構(gòu)造出物理地址衩侥。
如果在PTE中發(fā)現(xiàn)該虛擬頁沒有緩存在內(nèi)存,那么會觸發(fā)一個缺頁異常矛物。缺頁異常處理程序會把虛擬頁緩存進物理內(nèi)存茫死,并更新PTE。異常處理程序返回后履羞,CPU會重新加載這個虛擬地址峦萎,并進行翻譯。
虛擬內(nèi)存系統(tǒng)簡化了內(nèi)存管理忆首、鏈接爱榔、加載、代碼和數(shù)據(jù)的共享以及訪問權(quán)限的保護:
簡化鏈接雄卷,獨立的地址空間允許每個進程的內(nèi)存映像使用相同的基本格式搓蚪,而不管代碼和數(shù)據(jù)實際存放在物理內(nèi)存的何處。
簡化加載丁鹉,虛擬內(nèi)存使向內(nèi)存中加載可執(zhí)行文件和共享對象文件變得更加容易妒潭。
簡化共享,獨立的地址空間為操作系統(tǒng)提供了一個管理用戶進程和內(nèi)核之間共享的一致機制揣钦。
訪問權(quán)限保護雳灾,每個虛擬地址都要經(jīng)過查詢PTE的過程,在PTE中設(shè)定訪問權(quán)限的標(biāo)記位從而簡化內(nèi)存的權(quán)限保護冯凹。
操作系統(tǒng)通過將虛擬內(nèi)存與文件系統(tǒng)結(jié)合的方式谎亩,來初始化虛擬內(nèi)存區(qū)域,這個過程稱為內(nèi)存映射宇姚。應(yīng)用程序顯式分配內(nèi)存的區(qū)域叫做堆匈庭,通過動態(tài)內(nèi)存分配器來直接操作堆內(nèi)存。
你現(xiàn)在是否處于工作的瓶頸期浑劳,上不去下不來阱持,歡迎加入我們,十個月讓你完美蛻變魔熏,有興趣的朋友歡迎加QQ群:957734884了解詳情,還可以免費領(lǐng)取java的最新資料喲