07-堆
5.4.2 堆
堆用于处理更复杂的动态的内存分配需求。程序可能在某个时点在堆上分配内存,并且可能在某个其他时点释放,同时这些时点之间不存在严格的边界,就像堆栈内存一样。在堆栈上分配内存时,你能够确定分配和释放内存的时机。此外,堆中的值可能存活得比分配给它的函数更久,稍后也可能会被其他函数清理、释放。在这种情况下,代码无法调用free函数,因此最糟糕的情况可能是根本无法取消分配。
不同语言使用堆内存的方式也不尽相同。在Python等动态语言中,一切都是对象,默认情况下它们都会在堆上分配内存。在C语言中,我们使用手动调用malloc函数的方式在堆上分配内存,而在C++中,我们使用关键字new分配内存。要释放内存,我们需要在C语言中调用free函数,而在C++中调用delete函数。在C++中,为了避免手动调用delete函数,程序员经常使用诸如unique_ptr或shared_ptr这样的智能指针类型。这些智能指针类型具有析构方法,当它们超出内部作用域时,会调用delete函数。这种管理内存的范式被称为RAII原则,并由C++推而广之。
Rust对C++管理堆内存的机制也提供了类似的抽象。Rust在堆上分配内存的唯一方法是通过智能指针类型。Rust中的智能指针类型实现了Drop特征,它指定了如何释放值所使用的内存,并且在语义上定义了类似于C++中析构函数的方法。除非用户编写自定义的智能指针类型,否则你永远不需要在其类型上实现Drop特征。和Drop特征有关的详细内容有专门的部分予以论述。
为了在堆上分配内存,语言依赖于专用的内存分配器,它隐藏了所有底层细节,例如在已对齐的内存上分配内存,维护空闲的内存块以减少系统调用开销,并在分配内存和其他优化时减少碎片的产生。对于编译程序,编译器rustc自身会采用jemalloc内存分配器,而从Rust构建的库和二进制文件会使用系统内存分配器。在Linux上,依赖的将是glibc内存分配器API。jemalloc是一个支持多线程环境的高效内存分配器库,它大大减少了Rust程序的构建时间。虽然编译器采用了jemalloc,但是任何使用Rust构建的应用程序都不会使用它,因为它会增加二进制文件的大小。因此,已编译的二进制文件和库默认情况下都会采用系统内存分配器。
Rust还有一个可插拔的分配器设计,可以使用系统内存分配器,或实现std::alloc模块下的GlobalAlloc特征的自定义内存分配器。
注意
如果你想让自己的程序使用jemalloc软件包,那么可以访问jemalloc官网。
在Rust中,大部分事先不知道尺寸的动态类型都在堆上分配内存。不过这不包括基元类型。例如,在堆上创建一个String对象:
let s = String::new("foo");
String::new会在堆上分配一个Vec
在下一节中,让我们讨论C语言的一些不足之处,这种语言没有自动内存管理带来的便利。