堆空間和棧空間

Rust語言區分堆空間和棧空間,雖然它們都是內存中的空間,但使用堆和棧的方式不一樣,這也使得使用堆和棧的效率有所區別。

棧空間和棧幀

棧空間和棧幀都是屬於操作系統的概念,操作系統負責管理棧空間,負責創建、釋放棧幀。

棧空間採用後進先出的方式存放數據(就像疊盤子)。每次調用函數,都會在棧的頂端創建一個棧幀(stack frame),用來保存該函數的上下文數據。比如該函數內部聲明的局部變量通常會保存在棧幀中。當該函數返回時,函數返回值也保留在該棧幀中。當函數調用者從棧幀中取得該函數返回值後,該棧幀被釋放(實際上不會真的釋放棧幀的空間,無效的棧幀可以被複用)。

實際上,有一個ESP寄存器專門用來跟蹤棧幀,該寄存器中保存了當前最頂端的棧幀地址。當調用函數創建新的棧幀時(棧幀總是在棧頂創建),ESP寄存器的值更新為此棧幀的地址,當函數返回且返回值已被讀取後,該函數棧幀被移除出棧,出棧的方式很簡單,只需更新ESP寄存器使其指向上一個棧幀的地址即可。

不僅棧空間中的棧幀是後進先出的,棧幀內部的數據也是後進先出的。比如函數內先創建的局部變量在棧幀的底部,後創建的局部變量在棧幀的頂部。當然,上下順序並非一定會如此,這和編譯器有關,但編寫程序時可如此理解。

實際上,有一個EBP寄存器專門用來跟蹤調用者棧幀的位置。當在函數a中調用函數b時,首先創建函數a的棧幀,當開始調用函數b時,將在棧頂創建函數b的棧幀,並拷貝上一個ESP的值到EBP,這樣EBP寄存器就保存了函數a的棧幀地址,當函數b返回時通過EBP就可以回到函數a的棧幀。

在編寫代碼的時候,通常不考慮屬於操作系統的棧空間和棧幀的概念,而是這樣思考:有一塊內存,這塊內存中存放數據的方式是後進先出。比如,調用函數時,函數內部的局部變量可以說成【存放在棧中或棧空間中】,而不將其具體到【存放在該函數的棧幀中】。也就是說,此時可以混用棧和棧空間的說法,且重在描述(主要是為了將棧和堆區分開來)而不是側重於其準確性。後文也都如此混用棧和棧空間。

堆內存

不同於棧空間由操作系統跟蹤管理,堆內存是一片無人管理的自由內存區,需要時要手動申請,不需要時要手動釋放,如果不釋放已經無用的堆內存,將導致內存洩漏,內存洩漏過多(比如在某個循環內不斷洩漏),可能會耗盡內存。

手動申請、手動釋放堆內存是一件非常難的事,特別是程序較大時,判斷在何處編寫釋放內存的代碼更是難上加難。所以有一些語言提供了垃圾回收器(GC)來自動管理堆內存的回收。

Rust沒有提供GC,也無需手動申請和手動釋放堆內存,但Rust是內存安全的。這是因為Rust使用了自己的一套內存管理機制,只要能夠編譯通過,多數情況下可以保證程序沒有內存問題。

其中機制之一是作用域:Rust中所有的大括號都是一個獨立的作用域,作用域內的變量在離開作用域時會失效,而變量綁定的數據(無論綁定的是堆內數據還是棧中數據)則自動被釋放

fn main(){
  {   // 大括號,一個獨立的作用域
    let n = 33;
    println!("{}", n);
  }  // 變量n在此失效,其綁定的數據33被釋放
  // 此處無法再使用變量n
  // println!("{}", n);  // 編譯錯誤
}

關於Rust更多的內存管理機制(如所有權系統、生命週期等),放在後面的章節再解釋。