堆空间和栈空间

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更多的内存管理机制(如所有权系统、生命周期等),放在后面的章节再解释。