前言
在之前的文章当中,我们已经对 Rust 的一些基础数据类型和控制流有了一定的了解。但这些只能算是 Rust 入门的冰山一角,接下来我们就来看看在 Rust 当中最为独特的特性:所有权。一起来看 Rust 当中如何通过 所有权 让 Rust 无需垃圾回收(GC)就可以保证内存安全的。
什么是所有权
Rust 的核心特性就是所有权。
所有的程序在运行的时候,都必须要管理他们使用计算机内存的方式,有一些语言,如:JAVA,有垃圾回收机制,在程序运行时,不断地寻找不再使用的内存,并将其收集起来并释放。也有一些语言,如:C 或 C++ 则是需要我们在编写代码的时候,显式地分配和释放内存。
而我们熟悉的 JavaScript,也是一种具有自动垃圾回收机制编程语言。JavaScript 的垃圾回收机制主要基于一个叫做 "标记-清除"(mark-and-sweep)的算法。在这个算法中,垃圾回收器首先会标记那些仍然被引用的对象,然后清除那些没有被标记的对象。这样一来,被标记的对象会被保留,而未被标记的对象将会被释放。
在 JavaScript 中,当一个对象不再被引用时,垃圾回收机制会自动将其标记为可回收的垃圾对象。当需要释放内存时,垃圾回收机制会自动回收这些垃圾对象所占用的内存空间,然后将其重用给新的对象。
由于垃圾回收是自动进行的,开发者无需显式地释放对象或管理内存。这使得 JavaScript 更加便于使用,但也需要开发者注意避免创建大量不必要的对象,以免对性能造成不良影响。
而我们今天的主角 Rust,则是特立独行,没有采用以上的两种方式(垃圾回收机制、显式管理),而是开辟出了一条新的道路:所有权系统。
Rust 的内存是通过所有权系统来管理的,其中还包含了一组编译器在编译时的检查的规则。而 Rust 在编译时就会根据这个规则进行检查。由于这是在编译时运行的,因此,这个 所有权系统 不会带来任何的运行时开销。所以在程序运行的时候,所有权特性不会减慢程序的运行速度。
所有权的规则
- 每个值都有一个变量,这个变量就是该值的所有者
- 每个值同时只能有一个所有者
- 当所有者超出所在作用域时,这个值将会被删除掉
变量作用域(Scope)
作用域就是程序中一个项目的有效范围
fn main() {
// 这一行变量 a 还没有声明,因此不可用
let a = 0;// 这一行声明了变量 a,可以使用了
// 这一行到 } 为止,我们可以对 a 进行合理操作
}// 作用域结束了,a 又不可用了
栈内存(Stack)与堆内存(Heap)
我们在进行 JavaScript 的开发时,很少会关心我们的数据会被存储在 栈内存 还是 堆内存 当中,而在 Rust 这样的系统级的编程语言当中,一个值是存储在栈内存还是存储在堆内存对语言的行为是有重大影响的。
在我们程序运行的时候,无论是 栈内存 还是 堆内存,对于程序来说都是可用的内存,但是他们因为结构不同,因此他们的功能和存储的目标也不太一样。
栈内存(stack)
存储数据
相信栈这个数据结构大家都很清楚了,如果又不了解的同学,可以看一下本人以前整理的一篇文章:kiner算法刷题记(二):递归与栈(解决表达式求值问题)。总结一下,栈的特性就是先进后出
,从头部入栈,也从头部出栈。打个比喻,就像是羽毛球筒,我们平时要打羽毛球的时候,都会从最上面拿出一个羽毛球来打,打完之后,再把这个羽毛球塞会最上面去。
所有存储在栈内存中的数据,都必须拥有已知且固定的大小。那些在编译时大小未知或者是运行时大小会改变的数据,如:Vector,长度不固定,可以在运行时添加和删除元素,因此 vector 类型的数据会被存储在 堆内存当中,而非栈内存。
对于栈内存来说,不存在 “分配内存”一说,因为栈内存中存储的数据都是固定大小的,因此栈内存不需要分配内存,只需要将要存储的值在栈中依次存放即可。
在 Rust 当中,指针类型因为其大小也是固定的,因此也是可以被存放在栈内存当中的。而当我们想要获取该指针类型对应的实际存储的值的时候,只需要根据指针中存储的地址信息就可以找到对应的内存空间,将相关的值取出来。这就像是我们到饭店预定年夜饭,预定时会确定到时候最多会有多少人出席,然后服务员会根据人数给我们预留足够大的包厢(内存空间),并告诉我们包厢的名称(内存地址),到了年夜饭,我们只要告诉服务员预定的包厢是哪个(指针中存储的内存地址),就可以找到我们预定的包厢了。
将数据压入到堆内存当中会比将数据分配到堆内存中快的多。因为操作系统不需要再去寻找用来存储数据的空间了,直接将数据压入到栈顶即可。这就像你是某家饭店的股东之一,饭店给你长期预留了一个包厢,这个包厢不对外预定,只为你预留,你来了之后,直接过去就行了。
访问数据
栈内存的数据访问速度也是比堆内存会快很多的。因为 栈内存排列比较紧密,处理器处理的速度会更快。此外,由于栈内存通常存储的是一些实际的值,无需通过指针再去寻找真实的值。当然,我们上面也说了,栈内存也是可以用来存储指针的,如果访问的是指针类型的数据,我们就还得根据指针记录的内存地址访问实际内存空间才能找到实际存储的值了。
堆内存(heap)
存储数据
对于堆这样的数据结构,相信很多计算机专业的同学应该也都清楚,如果还有不了接的同学,也可以看一下之前整理的一篇你文章:05.1-堆(Heap)与优先队列(堆的数据结构基础篇)。总的来说,我们可以始终从堆顶获得最大/最小值。
在 Rust 当中,堆内存相较于栈内存而言,对内存的组织管理能力会差一些。当我们将数据存储到堆内存当中时,程序会请求一定数量的空间用于存储数据。操作系统会在堆内存中查找一个足够大的空间,并将其标记为正在使用,然后将这块空间的地址以指针的形式返回。这就是在 堆内存上的内存分配。
在 Rust 当中,堆内存中存储的通常都是指针,而不是真正的值,如上面说的 Vector
类型的数据,存放在 堆内存中时,实际存放的是这个数据存放位置的一个指针。
访问数据
上面说了堆内存访问的速度是比栈内存慢的,是因为堆相较于栈来说数据存储比较稀疏,处理器处理的速度也就会比较慢,并且在堆内存中通常存储的都是指针,也就是内存地址引用,我们需要获取实际的值的时候,还得通过指针查询到实际存放值的内存空间才能获取到值。还有就是,我们的堆内存存储数据是需要分配内存空间的,分配空间的操作也是需要耗费时间的。因此,在效率上并没有 栈内存 那么高。
何时使用何种内存方式
有同学可能会有疑问了,既然 栈内存 的访问速度远高于堆内存,为啥还要有一个堆内存的,堆内存具体应用再那些场景呢?除了上面说过的,栈内存只能存储大小固定的数据,大小不固定的数据只能存储在堆内存之外。我们的一些大数据的存储,也需要使用堆内存,否则都存在栈内存中,就会爆栈了。其实大家可以想一下,计算机原理。在计算机当中,CPU 寄存器的存储速度 > 内存 > 硬盘。那么为啥还需要存储速度慢的硬盘呢?其实也是为了用来储存大量的数据,我们可以这么理解,这就是低效设备就是高效设备的缓存。
结语
关于栈内存和堆内存的相关内容暂且先介绍到这里,我们会在在后续的文章中,用实际代码带大家看一下再程序运行过程中,究竟 栈内存 和 堆内存是如何工作的。