最近团队同学在内部分享了《V8 垃圾回收策略浅析》,着重探讨了目前各编程语言内存管理模型的设计,目前主要分为「自动」和「手动」两种做法,分享后的讨论中我提出了 rust ownership 是一种特殊的方式,之后我梳理学习了其机制,下面是文章正文。

在分享后的讨论时我提到:常见的内存管理方式除了「自动」和「手动」外还有一种特殊的,也就是 rust ownership(所有权),所有权是 Rust 最重要的特性之一。先通过下面这个例子简单看下:

  1. fn main() {
  2. let foo = String::from("hello");
  3. let bar = foo; // 字符串对象「hello」的所有权被转移
  4. println!("{}" foo); // error 无法再使用 foo
  5. }

在 Rust 中每个值都只能被一个所有者拥有,当这个值被赋给其他所有者,原所有者无法再使用。正是这种机制保证了 Rust 语言的内存安全,从而无需自动垃圾回收,也无需手动释放。像上面这个例子,当字符串对象被赋值给 bar 时,它的所有权被转移,foo 无法再使用,这和我们现有编程语言的认知(引用拷贝或值拷贝)产生了非常大的差异,但这在 Rust 语言中是完全正确的。
常见的手动管理内存语言如 C/C++ 等,需要通过 mallocfree 来手动申请和释放内存,好处是可以由程序开发者灵活的管理,但缺点也是这些逻辑与业务本身无关,会带来一些心智负担,如果处理不好会导致很严重的内存泄露(手动管理内存是大部分内存不安全的源头)。
而其他大部分自动管理内存的语言如 JavaScript/Java/Go/C# 等,都是在其运行时中内建了垃圾回收机制,通过程序的分析自动处理内存空间(Java 自称是内存安全语言),这么做的好处是内存管理相对更安全,减少了手动管理的心智负担,但缺点是一般会有一个不小的运行时(V8/JVM 等程序执行依赖的虚拟机,或者像 GoLang 一样直接将 GC runtime 编译到二进制产物内),同时在运行中标记垃圾内存以及回收内存(Stop The World)都有不小的损耗。

Rust 基于所有权机制,在编译阶段由编译器来负责处理内存管理。编译成功后,变量内存何时回收已经被确定,硬编码到二进制程序中,程序自己运行到该回收的时候就会自动回收。那么 Rust 是如何做到的呢?

作用域机制

同其他语言一样,Rust 的作用域也是 { 开始 } 结束,但在作用域结束时不仅回收栈上的变量,也回收堆上的内存:

  1. fn myScrope() {
  2. let foo = 2; // 在栈上分配 stack
  3. let bar = String::new(); // 在堆上分配 heap
  4. }

但如果 bar 被作为返回值 return 出去了(作用域改变)怎么办?return 出去后只是换到了另一个作用域内,当该作用域结束时依然会被回收。

所有权转移

上面已经有一个例子介绍 Rust 语言内的所有权(ownership)和所有权的转移(move),下面是一个稍微复杂点的:

  1. fn main() {
  2. let s = String::from("hello");
  3. myFunc(s); // 字符串的所有权转移到了 myFunc() 的内部
  4. let a = s; // error s 已经无法被使用
  5. }
  6. fn myFunc(s: String) {
  7. println!("{}", s); // s 将在离开作用域时被释放
  8. }

精通 JavaScript 的同学一定会在这里提出疑问:如果有闭包场景怎么处理所有权和内存回收?这里可以读一下这篇文章,解释的非常详细:《Rust 中的闭包与关键字 move》

所有权借用

有时变量值在作为函数参数使用后,在当前作用域仍要使用,函数结尾将其 return 是一个解决办法但不是好办法,这里 Rust 通过借用(borrow)来解决,先看一个例子:

  1. fn main() {
  2. let s = String::from("hello");
  3. let a = &s;
  4. println!("{}", s); // success
  5. println!("{}", a); // success, print "hello"
  6. func(&s); // success
  7. }
  8. fn func(s: &String) {
  9. println!("{}", s);
  10. }

变量 a 通过借用操作符 & 借用了 s 的内存,并没有转移,但现在 a 能访问 s 的空间,且允许有多个借用者,传入到函数 func() 的也是 s 的一个借用,会在 func() 结束时被释放。需要注意的是在被借用期间,拥有者不允许修改变量,或者转移所有权。

总结

通过上述所有权、转移、借用的机制,Rust 提供了一种全新的内存管理模型,它介于自动与手动之间,汲取了两者的优点,在编译期间处理内存管理,给后续其他编程语言有着很大的启发作用。
同时给大家分享一个 Rust 学习曲线,看看自己处于哪个阶段:
image.png