先前我们讲到Java等支持自动垃圾回收的语言由于需要在程序运行的时候启动垃圾回收器进行垃圾的扫描标记回收操作,所以会影响程序性能和流畅度。Rust也是不需要程序员手动回收内存的同时又具备C、C++语言的性能,它是怎么做到的呢?接下来我们就开始解密!
我们把从程序编写到运行简单分为三个阶段:编写,编译和执行。
编写阶段是程序员编写程序源代码的阶段,编译是把程序员编写的源代码翻译成机器可以读懂的二进制可执行文件的阶段,执行阶段是操作系统加载程序可执行文件并运行的阶段。
类似C的没有自动垃圾回收机制的语言需要程序员在编写代码的时候及时释放使用过的内存,这需要程序员时刻警惕有没有忘记回收内存,这增加了程序员的负担。
类似Java/Go的带有自动垃圾回收机制的语言是在程序运行时,启动一个垃圾回收器对程序中不再使用的内存进行扫描标记回收。垃圾回收器的运行是需要内存和计算资源的,这就加重了程序运行时的负担。
这两种语言要么是增加了程序员的负担影响开发效率要么是增加了程序运行负担影响性能。那怎么做到即不增加程序运行负担又不需要程序员去关注垃圾回收呢?
这就是Rust的独到创新的地方,Rust选择在程序编译为可执行文件时也就是编译阶段进行垃圾的判断和添加回收语句。简单来说就是原来我们使用完了一块内存需要调用drop方法释放掉这块内存:
let v = String::from(“这是一本书,书的名字是学习新时代编程语言Rust”);
drop(v);
使用Rust编写代码时我们就不需要编写drop(v); 这行代码了,当我们执行编译操作的时候Rust编译器会帮我们在合适的地方插入这段drop代码,是不是很神奇?
那Rust又是怎么在编译的时候判断一个变量所持有的内存使用完了该释放了呢?当然在实际开发中还需要考虑多个变量指向同一块内存等复杂情况。
这里我们需要先了解下程序的内存结构:程序使用的内存被分为俩类一类是栈、一类是堆,也就是栈内存和堆内存。栈内存可以想象成一个装球的桶:
一次只能往里放一个球一次也只能拿出一个最近放入的球。放球的动作称为压栈。拿出的动作称为出栈。栈内存由操作系统管理不需要程序管理,我们编写程序时主要考虑堆内存。有同学可能会问,那我们只用栈内存不用堆内存不就不需要考虑垃圾回收了吗?这里需要向大家解释下为什么需要堆内存。这主要由于栈内存只能用来存储长度固定的数据。比如长度为32位的整数类型i32、长度为64位的整数类型i64、浮点数类型f32、f64还有长度为32位的字符类型、8位的布尔类型这些基本变量数据类型他们所需要的长度都是固定的所以可以使用栈内存存储。
在实际开发中我们还需要存储一些长度不固定的内容,如要编写一个博客系统,我们需要存储我们编写的小说,在小说完成前我们是不知道需要多少空间存储的。甚至我们还会经常回来加一段减一段字。对于这种长度不固定长度会变动的数据,栈内存就无能为力了。可以把堆内存想象成一块操作系统管理的土地,程序就相当于土地产开发商。程序需要堆内存存储数据时就向操作系统申请,用完了要主动归还给操作系统。在这么大一块土地上开发商怎么知道那一块是自己的呢?这就需要一个凭证,这个凭证是开发商所拥有的土地的地址。由于凭证的长度是固定的所以在程序里这个凭证就被放在栈里。例如以下代码:
let v = String::from(“Hello”);
使用let声明一个变量v并使用String类型关联的from方法创建了一个值为”Hello”的字符串并赋值给变量v,这条语句在堆上分配了一块内存用于存放“Hello”,在栈上存放了指向堆内存地址的凭证里面包括堆内存的起始位置、长度和容量:
当前变量v拥有指向存储”Hello“堆内存的凭证,我们称这种关系为拥有关系,Rust就是通过判断堆内存的拥有者的生命周期来判断是否该释放它所拥有的堆内存的。比如这里的变量v,它的生命周期从创建语句开始到它所在的作用域也就是大括号(花括号)结束:
Rust编译器就可以在大括号结束前帮我们添加一条drop(v)语句释放它所持有的堆内存。拥有关系的概念是Rust的独到创新也是它实现垃圾回收的基础。
这里给大家留一个思考题:
如果声明一个变量i并赋值为1000,那它的内存结构是怎么样的呢?跟声明的v有什么区别呢?