基于语言之间的差异,我们可以将它们采用的内存管理策略大致分为3类:
手动型: C语言采用了这种内存管理机制,且完全由程序员负责,在程序代码使用完内存之后,调用 free
函数来释放内存。C++在某种程度上使用智能指针自动执行此操作,其中 free
函数调用放在类的析构函数定义中。Rust也有智能指针。
自动型: 采用这种内存管理形式的语言包括一个额外运行时线程,即GC,它作为守护线程与程序一起运行。自动化内存管理是使用这些语言编写代码很容易的原因之一。
半自动型: Swift等语言属于这一类别。它们没有作为运行时的一部分的内置专用GC,但提供了引用计数类型,这可以细粒度地实现自动化内存管理。Rust也提供了引用计数 Rc<T>
和 Arc<T>
程序运行时,进程中的内存分配即可能发生在 stack
上,也可能发生在 heap
上。
Rust偏向于使用stack内存。通常,你创建并绑定到变量的任何类型的值或实例都会存储到stack中。存储到heap上是显式的,可以通过智能指针类型来实现。
无论何时调用函数或方法,stack都用于为函数内部创建的值分配空间。函数中的所有 let
绑定都存储在stack中,它既可以是值本身,也可以是指向heap内存地址的指针。 这些值构成了活动函数的堆栈帧,堆栈帧是stack存储器的逻辑块,用于存储函数调用的上下文。该上下文可能包括函数参数、局部变量、返回地址,以及从函数返回需要恢复的任何易保存的寄存器值。随着越来越多的函数被调用,它们对应的堆栈帧会被压入stack。一旦函数返回,与之相关的堆栈帧,以及其中声明的所有值都会一起被清理释放。
这些值会根据它们声明的相反顺序删除,并且遵循后进先出(Last In First Out, LIFO)规则。
stack内存分配速度很快,因为分配和释放内存只需要一条CPU指令:递增/递减堆栈帧指针。堆栈帧指针(esp)是一个CPU寄存器,它始终指向stack的最顶端。堆栈帧指针在函数被调用或返回时实时更新。当函数返回时,通过将堆栈帧指针恢复到进入函数之前的位置丢弃该堆栈帧。使用stack是一种临时性内存分配策略,但由于其简单性,它在释放已使用内存方面是可靠的。但是,stack的相同属性不适用于超出当前堆栈帧生命周期的情况。
fn double_of(b: i32) -> i32 {
let x = 2 * b;
x
}
fn main() {
let a = 12;
let result = double_of(a);
}
heap用于处理更复杂的动态的内存分配需求。程序可能在某个时点在heap上分配内存,并且可能在某个其他时点释放,同时这些时点之间不存在严格的边界,就像stack一样。在stack上分配内存时,你能够确定分配和释放内存的时机。此外,heap中的值可能存活得比分配给它的函数更久,稍后也可能会被其他函数清理、释放。在这种情况下,代码无法调用 free
函数,因此最糟糕的情况可能是根本无法取消分配。
不同语言使用堆内存的方式也不尽相同。在C++中,为了避免手动调用 delete
函数,程序员经常使用诸如 unique_ptr
或 shared_ptr
这样的智能指针类型。这些智能指针类型具有析构方法,当它们超出内部作用域时,会调用delete
函数。这种管理内存的范式被称为RAII原则,并由C++推而广之。
Rust对C++管理堆内存的机制也提供了类似的抽象。Rust在heap上分配内存的唯一方法是通过智能指针类型。Rust中的智能指针实现了Drop特征,它指定了如何释放值所使用的的内存,并且在语义上定义了类似C++中析构函数的方法。
为了在heap上分配内存,语言依赖于专用得内存分配器。编译器rustc自身会采用jemalloc内存分配器,从而rust构建的库和二进制文件会使用系统内存分配器。在Linux上,依赖的将是glibc内存分配器的API。jemalloc是一个支持多线程环境的高效内存分配器库,它大大减少了Rust程序的构建时间。虽然编译器采用了jemalloc,但是任何使用Rust构建的应用程序都不会使用它,因为它会增加二进制文件的大小。因此,已编译的二进制文件和库默认情况下都采用系统内存分配器。
Rust还有一个可插拔的分配器设计,可以使用系统内存分配器,或实现std::alloc
模块下的GlobalAlloc特征的自定义内存分配器。
在Rust中,大部分事先不知道尺寸的动态类型都在heap上分配内存。不过这不包括基元类型。
let s = String::new("foo");
String::new
会在heap上分配一个 Vec<u8>
类型,并返回对它的引用。此引用会和变量s
绑定,该变量在stack上分配内存。只要 s
在作用域内,heap中的字符串就一直存在。当 s
超出其作用域时,Vec<u8>
将会从heap释放,其 drop
方法将作为 Drop
实现的一部分进行调用。对于需要在heap上为基元类型分配内存的极个别情况,可以使用 Box<T>
类型,它是一种泛型智能指针类型。
内存安全性是指你的程序永远不会访问它不应该访问的位置,程序中声明的变量不能指向无效内存,并且在所有代码路径中保持有效。换句话说,安全性基本上会归结为在程序中始终具有有效引用的指针,并且使用指针的操作不会导致未定义的行为。未定义的行为指程序的状态出现了编译器未考虑到的情况,因为编译器规范中没有说明在该情况下会发生什么。
内存安全性Bug会导致内存泄漏,以分段错误的形式导致程序崩溃,或者在最糟糕的情况下产生安全漏洞。要在C语言中创建正确且安全的程序,程序员必须在使用完内存后进行适当的free
函数调用。如今的C++通过智能指针类型来处理与手动内存管理有关的问题,但这并不能完全消除它们。基于虚拟机的语言(JVM是最典型的例子)使用垃圾收集器来消除所有和类有关的内存安全问题。虽然Rust没有内置GC,但由于该语言中采用了相同的RAII原则,同时根据变量的作用域为我们自动释放使用过的内存,因此比C/C++更安全。它为我们提供了几个细粒度的抽象,用户可以根据自己的需要进行选择。
Rust能够在编译期检测程序中内存安全违规,在离开作用域时自动释放相关资源等情况。我们将这些概念称作所有权、借用和生命周期。
所有权有点儿类似核心原则,而借用和生命周期是对语言类型系统的扩展。在代码的不同上下文中加强或有时放松所有权原则,可确保编译器内存管理正常运作。
所有权、借用和生命周期详情,请参考所有权章节详细查看。
☰