一文徹底搞懂“內(nèi)存管理”
原創(chuàng)【51CTO.com原創(chuàng)稿件】筆者面試過(guò)不少業(yè)務(wù)后臺(tái)開(kāi)發(fā)候選人,當(dāng)問(wèn)起內(nèi)存管理的相關(guān)問(wèn)題時(shí),往往都會(huì)答出 JVM 的垃圾回收機(jī)制,并對(duì) Serial、Parallel、CMS 等收集器如數(shù)家珍,侃侃而談。
圖片來(lái)自 包圖網(wǎng)
然而對(duì)于應(yīng)用層以下的內(nèi)存管理機(jī)制卻鮮有人能答出來(lái),甚至于認(rèn)為 JVM 直接管理了物理內(nèi)存。
誠(chéng)然,許多的高級(jí)語(yǔ)言如 Java、Go、Python 等已經(jīng)內(nèi)置了完善的自動(dòng)內(nèi)存管理機(jī)制,開(kāi)發(fā)者可以"開(kāi)箱即用"。
但如果只知其表,不知其里,在出現(xiàn)系統(tǒng)性能問(wèn)題時(shí)往往手足無(wú)措,無(wú)法全面思考解決問(wèn)題。
今天筆者嘗試從 0 開(kāi)始,用一篇文章講明內(nèi)存管理。
V1.0:直接使用物理地址
最開(kāi)始的時(shí)候,計(jì)算機(jī)只允許運(yùn)行一個(gè)進(jìn)程,內(nèi)存也只有幾百 KB 大小,那時(shí)候的世界很簡(jiǎn)單也很美好,保留一部分內(nèi)存空間給 OS 使用,剩下的都是這個(gè)進(jìn)程的專屬空間,想怎么用怎么用,如圖 1-1。
但為了更高效地利用 CPU 的計(jì)算資源,OS 需要支持"同時(shí)"運(yùn)行多個(gè)進(jìn)程,此時(shí)內(nèi)存空間按固定大小被瓜分為幾塊,分屬于各個(gè)進(jìn)程使用,如圖 1-2。
由于是直接使用內(nèi)存物理地址,如果這些進(jìn)程都很"本分",只訪問(wèn)自己的空間,那么一切都還正常,但如果某個(gè)進(jìn)程闖入他人的領(lǐng)地,胡作非為呢?可控性是個(gè)問(wèn)題。
V2.0:增加抽象轉(zhuǎn)換層,使用虛擬地址
當(dāng)考慮到增加管控、安全校驗(yàn)、動(dòng)態(tài)分配等問(wèn)題時(shí),增加一層抽象進(jìn)行"代理"往往是一個(gè)通用的解決方案。
到 2.0,進(jìn)程不再被允許直接使用物理內(nèi)存空間,而是使用從 0 開(kāi)始編碼的虛擬地址,經(jīng)由 MMU(Memory Management Unit)轉(zhuǎn)換得到實(shí)際地址,然后才能到內(nèi)存中獲取到數(shù)據(jù)。
中間層 MMU 會(huì)檢查虛擬地址的有效性和合法性,從而保證安全性。
考慮到內(nèi)存空間使用的靈活性,內(nèi)存按固定大小進(jìn)行分頁(yè)(Paging),通常是 4KB,連續(xù)的虛擬地址頁(yè)(VP,Virtual Page),映射到物理地址頁(yè)(PP, Physical Page)上,可以是分散的,這種靈活的設(shè)計(jì)可以提升物理內(nèi)存的空間利用率,減少內(nèi)存碎片。
既然有映射,自然需要存儲(chǔ)映射關(guān)系表,即頁(yè)表(Page Table),Key 值是虛擬地址頁(yè)號(hào)(Virtual Page Number)。
Value 值是包含有物理地址頁(yè)號(hào)(PPN,Physical Page Number)的數(shù)據(jù)結(jié)構(gòu)(PTE,Page Table Entry),值得一提的是,頁(yè)表不存在 MMU 里面,同樣也是存在內(nèi)存里。
圖 2-1 簡(jiǎn)要地展示了虛擬地址到物理地址的轉(zhuǎn)換過(guò)程:
為了方便說(shuō)明,這里頁(yè)大小設(shè)置為 16 字節(jié)(2^4,offset 占用 4bit),總的物理內(nèi)存大小有 8 頁(yè)(2^3,PPN 占用 3bit)即 128 字節(jié),虛擬內(nèi)存至多使用 4 個(gè)頁(yè)(2^2,vpn 占用 2bit)。
MMU 將一個(gè) 6bit 的虛擬地址轉(zhuǎn)化為 7bit 的物理地址,其中通過(guò)頁(yè)表完成 vpn 到 PPN 的轉(zhuǎn)換,而 offset 部分保持不動(dòng)。
①V2.1 時(shí)間優(yōu)化:增加 TLB 緩存
在計(jì)算機(jī)領(lǐng)域,當(dāng)考慮性能提升的問(wèn)題時(shí),使用緩存是個(gè)萬(wàn)金油般的解決方案。
其背后主要是基于時(shí)空局限性理論(temporal/spatial locality):時(shí)間上,一個(gè)剛被訪問(wèn)過(guò)的數(shù)據(jù),很可能在不久之后被再次訪問(wèn);空間上,一個(gè)剛被訪問(wèn)過(guò)的空間 x,很可能在不久之后 x 的鄰近空間也被訪問(wèn)。
很自然地,我們可以在 MMU 里面加入一小塊緩存空間,即快表 TLB(Translation Lookasid Buffer),里面保存著最近的 vpn->PPN 映射關(guān)系。
如果緩存命中(TLB Hit),將極大地提升地址轉(zhuǎn)換速度,如果緩存未命中(TLB Miss),則重新從頁(yè)表中查詢。
遺憾的是,空間和時(shí)間永遠(yuǎn)是一對(duì)矛盾,TLB 容量越大,訪問(wèn)速度也隨著降低,你無(wú)法實(shí)現(xiàn)一個(gè)足夠大的 TLB 去替換掉內(nèi)存上的頁(yè)表,因此當(dāng) TLB 快滿時(shí),通常會(huì)使用近似 LRU 的算法將最少被使用的單元踢除。
圖 2-2 和 2-3 分別展示了 TLB 命中和未命中情況下的流程,如果命中,則只需一次物理內(nèi)存訪問(wèn);如果未命中,則會(huì)先到物理內(nèi)存中查詢 PTE,并更新至 TLB,然后再訪問(wèn)真正的數(shù)據(jù)地址。
②V2.2 空間優(yōu)化:多級(jí)頁(yè)表和交換分區(qū)
進(jìn)行時(shí)間優(yōu)化后,我們?cè)賮?lái)思考空間上有哪些可以優(yōu)化的。我們注意到,原始的線性頁(yè)表會(huì)隨著虛擬內(nèi)存的增大而增大。
試算一下,一個(gè) 32bit 大小的虛擬地址(2^32),分頁(yè)大小為 4KB(2^12),則會(huì)有 1M 個(gè)分頁(yè) (2^20)。
假如一個(gè)映射單元 PTE 占用 4 個(gè)字節(jié),則光存儲(chǔ)這個(gè)進(jìn)程的映射表就需要 4MB。
如果機(jī)器上同時(shí)運(yùn)行了 100 個(gè)進(jìn)程,那么將吃掉 400MB 大小的內(nèi)存空間!這對(duì)于整個(gè)系統(tǒng)來(lái)說(shuō)將是極大的浪費(fèi)。
避免這種浪費(fèi)的關(guān)鍵在于,并非所有的虛擬分頁(yè)都需要保存其映射關(guān)系,對(duì)于還未被使用的分頁(yè)群,可以只使用一個(gè) PTE(Page Table Entry)表示,而對(duì)于連續(xù)使用的分片群,可以使用多級(jí)映射來(lái)定位。
圖 2-4 展示了二級(jí)頁(yè)表的尋址過(guò)程,圖中一級(jí)頁(yè)表的一個(gè) PTE 可以代表 1 千個(gè) VP。
這樣對(duì)于中段大量空閑的 VP,只需使用若干個(gè) PTE 即可表示,顯著地減小了頁(yè)表的總大小,對(duì)于大容量且稀疏的虛擬地址空間,可以依此類推,再增加幾級(jí)頁(yè)表。
為了更高效使用我們珍貴的內(nèi)存空間,除了通過(guò)多級(jí)頁(yè)表節(jié)流之外,我們還能通過(guò)使用部分磁盤(pán)空間,即交換分區(qū),作為虛擬內(nèi)存來(lái)達(dá)到開(kāi)源的效果。
具體來(lái)說(shuō),我們提供給上層應(yīng)用的虛擬內(nèi)存空間是可以大于實(shí)際可用的內(nèi)存空間的。
只要 OS 時(shí)不時(shí)將一些不常用的內(nèi)存數(shù)據(jù)復(fù)制到交換分區(qū)然后從內(nèi)存清除,就可以源源不斷地提供新的內(nèi)存空間。
當(dāng)讀取到這部分虛擬內(nèi)存時(shí),再?gòu)慕粨Q分區(qū)恢復(fù)到內(nèi)存就可以了,當(dāng)然了,這種操作會(huì)一定程度上降低內(nèi)存的讀寫(xiě)速度。
圖 2-5 展示了增加了交換分區(qū)后的工作流程,當(dāng) OS 發(fā)現(xiàn)要查找的 PTE 既不在 TLB 中,也不在內(nèi)存中,就會(huì)拋出一個(gè) Page Fault 異常,OS 再異步地從交換分區(qū)中查找出 PTE 并寫(xiě)回內(nèi)存,完成后 CPU 再發(fā)起重試就可以了。
V3.0:無(wú)招勝有招,自動(dòng)管理內(nèi)存
通過(guò)上述的設(shè)計(jì),操作系統(tǒng)為上層應(yīng)用搭建了一個(gè)安全舒適的虛擬樂(lè)園,在這個(gè)樂(lè)園里面,應(yīng)用無(wú)需關(guān)注真實(shí)的內(nèi)存轉(zhuǎn)換、尋址等繁瑣事項(xiàng),只管在需要時(shí) malloc 申請(qǐng)內(nèi)存,不需要時(shí) free 掉即可。
然而隨著應(yīng)用復(fù)雜度的快速上升,即使是自己的一畝三分地,也常常因?yàn)槭杪┗蛘?Bug 導(dǎo)致申請(qǐng)的內(nèi)存未及時(shí)釋放,造成內(nèi)存泄露最終導(dǎo)致應(yīng)用崩潰。
由此以 JVM 為代表的一系列自動(dòng)內(nèi)存管理平臺(tái)應(yīng)運(yùn)而生,通過(guò)定期掃描內(nèi)存中的數(shù)據(jù)對(duì)象,使用引用計(jì)數(shù)法或者可達(dá)性分析,區(qū)分出數(shù)據(jù)對(duì)象是否可回收,再結(jié)合標(biāo)記-清除算法、復(fù)制算法等實(shí)現(xiàn)內(nèi)存垃圾回收。
關(guān)于垃圾回收器的具體實(shí)現(xiàn)業(yè)界仍在不斷地更迭出新,這里不再細(xì)述。
結(jié)語(yǔ)
本文嘗試從最基礎(chǔ)的設(shè)計(jì)開(kāi)始,逐步引入虛擬地址轉(zhuǎn)換,隨后進(jìn)行時(shí)間和空間上的優(yōu)化,最后介紹應(yīng)用層的自動(dòng)內(nèi)存管理機(jī)制,循序漸進(jìn),希望能幫你構(gòu)建出一幅內(nèi)存管理的基本藍(lán)圖。
當(dāng)然,基于篇幅的限制,真實(shí)的系統(tǒng)設(shè)計(jì)細(xì)節(jié)遠(yuǎn)比本文介紹復(fù)雜得多,會(huì)引入更多層級(jí)的緩存、映射,并基于硬件特性做更多的優(yōu)化策略以提升內(nèi)存使用效率。
但大道至簡(jiǎn),理解其最核心的設(shè)計(jì)思路,再去看技術(shù)細(xì)節(jié),相信會(huì)幫你更快地理解領(lǐng)悟。
作者:李騰輝
簡(jiǎn)介:Akulaku 高級(jí)開(kāi)發(fā)工程師,目前負(fù)責(zé)金融借貸平臺(tái)架構(gòu)設(shè)計(jì)及核心建設(shè)工作,對(duì)微服務(wù)體系、JVM 虛擬機(jī)及操作系統(tǒng)原理機(jī)制有較深入的研究,擅長(zhǎng)定位并解決線上疑難問(wèn)題。
編輯:陶家龍
征稿:有投稿、尋求報(bào)道意向技術(shù)人請(qǐng)?zhí)砑有【幬⑿?gordonlonglong
【51CTO原創(chuàng)稿件,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文作者和出處為51CTO.com】