熱線電話:0755-23712116
郵箱:contact@shuangyi-tech.com
地址:深圳市寶安區(qū)沙井街道后亭茅洲山工業(yè)園工業(yè)大廈全至科技創(chuàng)新園科創(chuàng)大廈2層2A
內(nèi)存,以及編程語言如何管理內(nèi)存,是一個(gè)讓開發(fā)者們頭疼不已的問題。我們所寫的程序時(shí)刻不停地分配著內(nèi)存,但我們卻很難搞清楚,這一切到底是怎么發(fā)生的。
存儲(chǔ)空間,正如它一開始所定義的,是我們存儲(chǔ)特定信息,以備之后使用的地方,這種存儲(chǔ)可能是永久的(直到我們手動(dòng)刪除),也可能是臨時(shí)的(直到電腦自動(dòng)刪除)。實(shí)際上,我們和電腦之間的每一次交互,都涉及信息的存儲(chǔ)。比如說,打開一個(gè)瀏覽器時(shí),它的執(zhí)行步驟就從永久存儲(chǔ)(硬盤)加載到臨時(shí)存儲(chǔ)(內(nèi)存RAM)中。
主存儲(chǔ),或者說 RAM,是電腦使用的內(nèi)部存儲(chǔ)空間,有別于 USB 、硬盤之類的外部存儲(chǔ)設(shè)備。電腦可以與內(nèi)存直接交互,所有程序也必須加載到內(nèi)存中才能執(zhí)行。有時(shí),整個(gè)程序都會(huì)被加載到內(nèi)存中,也有時(shí),只有程序的一部分(一個(gè)進(jìn)程)被加載到內(nèi)存中——這個(gè)機(jī)制被叫做動(dòng)態(tài)加載。如果這部分程序依賴于另一個(gè)程序,那么,還會(huì)有一個(gè)動(dòng)態(tài)鏈接機(jī)制建立起這個(gè)程序與主程序之間的關(guān)系。
內(nèi)存管理影響到電腦中的每一個(gè)程序,極為關(guān)鍵,因此,現(xiàn)代操作系統(tǒng)都有一套復(fù)雜的機(jī)制來完成這項(xiàng)工作。通過各個(gè)層次(硬件層、操作系統(tǒng)層、應(yīng)用軟件層)的協(xié)調(diào)與控制,確保內(nèi)存使用合理高效。
本文聚焦于操作系統(tǒng)與應(yīng)用軟件中內(nèi)存管理。在系統(tǒng)層,內(nèi)存管理主要涉及特定存儲(chǔ)塊(可以被理解為地址與空間)的分配;在應(yīng)用層,內(nèi)存管理主要涉及向系統(tǒng)發(fā)送內(nèi)存空間請(qǐng)求,以及確保程序定義的對(duì)象與數(shù)據(jù)結(jié)構(gòu)有足夠的存儲(chǔ)空間(內(nèi)存的分配、重新分配以及釋放)。
當(dāng)一個(gè)程序申請(qǐng)一段內(nèi)存時(shí),一個(gè)“分配者”會(huì)負(fù)責(zé)將內(nèi)存分配給它,并在不再需要的時(shí)候釋放出來,以供重新分配。這個(gè)過程可以手動(dòng)控制,也可以自動(dòng)完成,主要取決于編程語言的特性以及程序員自己的選擇。
手動(dòng)內(nèi)存管理可以理解為程序員通過自己的代碼分配或釋放內(nèi)存。比較著名的,是 C 語言使用的動(dòng)態(tài)內(nèi)存分配技術(shù)。不過,得力于 ObjectiveC 和 Swift 的大力推廣,現(xiàn)在流行的大多數(shù)編程語言都通過垃圾回收器或自動(dòng)引用計(jì)數(shù)(ARC)實(shí)現(xiàn)了自動(dòng)內(nèi)存管理。
錯(cuò)誤的內(nèi)存操作會(huì)破壞內(nèi)存區(qū)塊的分配與釋放過程,導(dǎo)致很嚴(yán)重的后果。從更高層面看,內(nèi)存區(qū)塊總是會(huì)恢復(fù)正常的,一個(gè)簡單的錯(cuò)誤似乎并沒有那么嚴(yán)重,但系統(tǒng)中總是同時(shí)運(yùn)行著成百上千個(gè)進(jìn)程,不可能都卡在那里,等著某個(gè)內(nèi)存區(qū)塊恢復(fù)正常。
于是,這些錯(cuò)誤會(huì)用光程序運(yùn)行所需的必要內(nèi)存空間,或者更糟糕的是,如果區(qū)塊被錯(cuò)誤地釋放或分配,區(qū)塊中存儲(chǔ)的敏感信息,比如密碼、密鑰或者其它隱私信息,會(huì)被攻擊者所竊取。
以下是錯(cuò)誤的內(nèi)存操作產(chǎn)生的常見后果:
由于錯(cuò)誤的算術(shù)計(jì)算,原來分配的內(nèi)存區(qū)塊無法存儲(chǔ)最后的結(jié)果。比如說,一個(gè)程序可能定義了一個(gè)占用 8 位內(nèi)存的值,只能存儲(chǔ) -128 到 +127 之間的數(shù)字,假設(shè)程序先將這個(gè)數(shù)字賦值為 127,之后又加了 1,就會(huì)導(dǎo)致一個(gè)預(yù)期外的結(jié)果,因?yàn)?8 位內(nèi)存空間無法存儲(chǔ) 128 這個(gè)值。
這個(gè) Bug 由 Brumley, Chiueh 和 Johnson 在 2012 年定義,具體描述是,“一個(gè)變量的值超出了機(jī)器存儲(chǔ)這個(gè)值所用字節(jié)的表示范圍”。產(chǎn)生這個(gè) Bug 的原因很多,比如向上溢出、向下溢出、數(shù)據(jù)截取、符號(hào)錯(cuò)誤等,主要是由于錯(cuò)誤定義的語句或整數(shù)操作,而程序員要定位問題往往很困難。不同語言處理這個(gè)問題的方式也不一樣——例如,Smalltalk 與 Scheme 會(huì)自動(dòng)升級(jí)變量類型,而其它一些語言則把問題留給程序員自己。
如果一個(gè)程序一直向系統(tǒng)申請(qǐng),但不釋放內(nèi)存——也就是說,告訴系統(tǒng)哪些內(nèi)存可以重新利用了——就會(huì)導(dǎo)致內(nèi)存泄露,程序最終會(huì)用完所有可用內(nèi)存。另外,如果程序中的某個(gè)對(duì)象被存儲(chǔ)在內(nèi)存中,但運(yùn)行中的代碼實(shí)際上已經(jīng)沒法訪問到它了,也會(huì)導(dǎo)致同樣問題。
當(dāng)某個(gè)程序訪問它沒有權(quán)限訪問的、另作它用的內(nèi)存空間,或者對(duì)某部分內(nèi)存執(zhí)行超越權(quán)限的操作,比如試圖對(duì)只讀內(nèi)容進(jìn)行寫操作時(shí),就會(huì)導(dǎo)致段錯(cuò)誤。段錯(cuò)誤可能導(dǎo)致程序掛起、崩潰或退出。
當(dāng)程序要寫入的內(nèi)容超過了被分配的空間長度,它繼續(xù)寫入到之后的,另作它用,或者沒有寫權(quán)限的內(nèi)存空間時(shí),就會(huì)導(dǎo)致緩沖區(qū)溢出。緩沖區(qū)溢出也會(huì)使程序掛起、崩潰或退出。
當(dāng)程序試圖刪除一個(gè)已經(jīng)被刪除的對(duì)象,因而導(dǎo)致堆污染或者段錯(cuò)誤時(shí),就叫刪除錯(cuò)誤。刪除錯(cuò)誤也可以認(rèn)為是段錯(cuò)誤的一個(gè)子集。
對(duì)程序員來說,最常見的內(nèi)存問題就是如何操作內(nèi)存的問題——如果說系統(tǒng)可以把內(nèi)存分配給程序,那么,程序所使用的編程語言是手動(dòng)還是自動(dòng)完成內(nèi)存分配的呢?以及更重要的,這種分配方式會(huì)導(dǎo)致什么結(jié)果呢?
手動(dòng)內(nèi)存管理是指在特定語言中,程序員必須通過自己的代碼來管理內(nèi)存,與之相對(duì)地,自動(dòng)內(nèi)存管理是指程序員不需要或基本不需要執(zhí)行什么動(dòng)作來操作內(nèi)存。我們這里所說的“操作”和“管理”,是指申請(qǐng)、重新分配內(nèi)存,或者釋放掉我們認(rèn)為已經(jīng)成為“垃圾”的內(nèi)存空間。
直到上世紀(jì) 90 年代中期,主流編程語言都支持手動(dòng)內(nèi)存管理,即使在今天也依然如此(以關(guān)鍵詞 “new” 或 “alloc” 的形式)。不過,這僅僅是因?yàn)閷?duì)象創(chuàng)建,也就是為對(duì)象分配內(nèi)存的過程很容易而已——程序員在創(chuàng)建對(duì)象的時(shí)候,可以清楚地知道對(duì)象的大小、名稱以及初始化過程。然而,銷毀對(duì)象就困難多了,由于銷毀過程往往在對(duì)象創(chuàng)建很久之后才觸發(fā),程序員可能并不知道對(duì)象的大小。更麻煩的是,程序員可能也不知道具體在哪個(gè)時(shí)間點(diǎn)應(yīng)該銷毀對(duì)象,很有可能,軟件中的某部分代碼依然在使用這個(gè)對(duì)象。
如之前所說,如果不能正確地初始化或銷毀對(duì)象,就會(huì)導(dǎo)致內(nèi)存錯(cuò)誤。編程語言如何處理內(nèi)存錯(cuò)誤取決于它的具體實(shí)現(xiàn):大多情況下,內(nèi)存錯(cuò)誤會(huì)導(dǎo)致“未定義行為(undefined behavior)”——也就是說,說不準(zhǔn)會(huì)發(fā)生什么。(注意,在準(zhǔn)確的手動(dòng)內(nèi)存管理下,一切都是確定的,程序員總是清楚一個(gè)對(duì)象什么時(shí)候被創(chuàng)建或被銷毀。)
1959 年,一個(gè)內(nèi)存管理的新概念——垃圾回收——被引入 Lisp 編程語言。垃圾回收是自動(dòng)內(nèi)存管理中最著名的一個(gè)例子,通過垃圾回收,之后不再使用的對(duì)象會(huì)被銷毀,空間會(huì)被釋放。這種技術(shù)減少了 Bug,提高了內(nèi)存管理水平。垃圾回收的具體實(shí)現(xiàn)采用了多種策略,包括對(duì)象追蹤、引用計(jì)數(shù)、時(shí)間戳、心跳等。
其它自動(dòng)內(nèi)存管理技術(shù)包括基于棧的內(nèi)存管理(stack-based memory allocation)、基于作用域的內(nèi)存管理(region-based memory management)、自動(dòng)引用計(jì)數(shù)(ARC)等。不過,這些技術(shù)都存在一些性能問題,也帶來了某種不確定性,因?yàn)槌绦騿T并不能準(zhǔn)確地知道對(duì)象是在什么時(shí)候被銷毀的。
當(dāng)然,手動(dòng)內(nèi)存管理與自動(dòng)內(nèi)存管理都還被今天的編程語言廣泛應(yīng)用:前者以 C 語言家族為代表,后者以 Lisp、Java 以及其它眾多語言為代表。事實(shí)上,大多數(shù)語言都混合使用這兩種技術(shù):如前文所說,通過手動(dòng)方式分配內(nèi)存,通過垃圾回收技術(shù)釋放內(nèi)存。
如我們所見,電腦幫助人類解決復(fù)雜問題的方式,讓程序員有一種“宇宙之主”的感覺。我們也注意到,這個(gè)宇宙存在著種種規(guī)則和限制,其中一個(gè),就是內(nèi)存總是有限的。不過,正如哈姆雷特所說,作為程序員,我們依然可以“藏身果殼之中,而把自己看作擁有無限疆域的君王?!?/p>