1. 引言
此前的文章中,我们介绍了 Rust 的所有权:
Rust 的所有权机制
所有权机制让 Rust 可以方便地实现内存的自动回收,但是 Rust 究竟是如何来划分和管理内存的呢?本文来介绍一下。
2. 内存的分配 -- 堆和栈
和很多其他语言一样,Rust 也将内存换分为堆和栈两个部分。由于 Rust 语言是一种系统级编程语言,我们在编写过程中是必须要清楚到底内存是被分配到堆空间还是栈空间的,不过通常,在栈中放置数据并不称为“分配”,而是“压入”。
在 Rust 中,只有在编译期已知且固定大小的数据会被分配在栈空间上,而那些编译期无法确定大小的数据,则只能被放置在堆空间中。
举个例子来说,Rust 中的字符串有两种类型:
代码语言:javascript复制// &str 类型
let str1 = "hello world!";
// String 类型
let str2 = String::from("hello");
str1 是 &str
类型,它的值是大小固定且内容不可变的,他在编译期已经可以确定使用内存的大小,因此,str1 会被压到栈上。
str2 是 String
类型,这是一种 Rust 的封装类型,它是可变的字符串类型,例如,你可以通过下面的方法为 str2 添加新的内容:
str2.push_str(" world!");
因此,String 类型的 str2 是被分配在堆空间的,尽管如此,实际上,在栈空间中仍然会压入一个结构,用来保存指向堆空间的指针、此次分配堆空间的容量,以及已使用长度。
3. 内存的释放
由于堆空间是在运行时动态分配的,所以和许多其他语言一样,堆空间的清理也是我们需要考虑的问题,Rust 的所有权机制很大程度上解决了这个问题。
当变量离开作用域时,根据所有权机制,Rust 会自动调用一个名为 drop
的特殊函数,在这个函数中,Rust 会释放所有不在被所有的内存。
此前的文章中,我们介绍了 Rust 的“移动”机制,简单的来说,就是对于基础类型,当把一个变量赋值给另一个变量时,Rust 会为新的变量在栈空间开辟一个新的空间,将原值复制一份,从而让两个变量在当前作用域内均为可用,这里说的“基础数据类型”只包括:
- 所有整数类型,包括有符号、无符号的类型,如 u32、i64 等;
- 布尔类型 bool;
- 字符类型 char。
而对于在堆空间中分配的数据来说,当把一个变量赋值给另一个变量时,Rust 会销毁原变量,数据的所有权被移动
到了新的变量上。
这样的差别是为什么呢?假设保存着指向堆空间中数据的变量内容复制一份给新的变量,那么就会出现一个堆数据被两个指针指向的情况。当两个变量退出作用域时,Rust 就必须判断究竟要释放这个堆数据几次,并且在这样的情况下,修改一个变量就意味着另一个变量指向的实际数据发生了修改,行为仍然与基础类型不同。事实上,最好的解决办法是同步复制堆空间中的数据,也就是其他很多语言中的“深拷贝”,但这样一来性能势必受到极大地影响。因此,经过权衡,Rust 通过“移动”的策略来实现了堆空间变量的赋值。