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