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