最近团队同学在内部分享了《V8 垃圾回收策略浅析》,着重探讨了目前各编程语言内存管理模型的设计,目前主要分为「自动」和「手动」两种做法,分享后的讨论中我提出了 rust ownership 是一种特殊的方式,之后我梳理学习了其机制,下面是文章正文。
在分享后的讨论时我提到:常见的内存管理方式除了「自动」和「手动」外还有一种特殊的,也就是 rust ownership(所有权),所有权是 Rust 最重要的特性之一。先通过下面这个例子简单看下:
fn main() {
let foo = String::from("hello");
let bar = foo; // 字符串对象「hello」的所有权被转移
println!("{}", foo); // error 无法再使用 foo
}
在 Rust 中每个值都只能被一个所有者拥有,当这个值被赋给其他所有者,原所有者无法再使用。正是这种机制保证了 Rust 语言的内存安全,从而无需自动垃圾回收,也无需手动释放。像上面这个例子,当字符串对象被赋值给 bar 时,它的所有权被转移,foo 无法再使用,这和我们现有编程语言的认知(引用拷贝或值拷贝)产生了非常大的差异,但这在 Rust 语言中是完全正确的。
常见的手动管理内存语言如 C/C++ 等,需要通过 malloc
和 free
来手动申请和释放内存,好处是可以由程序开发者灵活的管理,但缺点也是这些逻辑与业务本身无关,会带来一些心智负担,如果处理不好会导致很严重的内存泄露(手动管理内存是大部分内存不安全的源头)。
而其他大部分自动管理内存的语言如 JavaScript/Java/Go/C# 等,都是在其运行时中内建了垃圾回收机制,通过程序的分析自动处理内存空间(Java 自称是内存安全语言),这么做的好处是内存管理相对更安全,减少了手动管理的心智负担,但缺点是一般会有一个不小的运行时(V8/JVM 等程序执行依赖的虚拟机,或者像 GoLang 一样直接将 GC runtime 编译到二进制产物内),同时在运行中标记垃圾内存以及回收内存(Stop The World)都有不小的损耗。
Rust 基于所有权机制,在编译阶段由编译器来负责处理内存管理。编译成功后,变量内存何时回收已经被确定,硬编码到二进制程序中,程序自己运行到该回收的时候就会自动回收。那么 Rust 是如何做到的呢?
作用域机制
同其他语言一样,Rust 的作用域也是 {
开始 }
结束,但在作用域结束时不仅回收栈上的变量,也回收堆上的内存:
fn myScrope() {
let foo = 2; // 在栈上分配 stack
let bar = String::new(); // 在堆上分配 heap
}
但如果 bar 被作为返回值 return 出去了(作用域改变)怎么办?return 出去后只是换到了另一个作用域内,当该作用域结束时依然会被回收。
所有权转移
上面已经有一个例子介绍 Rust 语言内的所有权(ownership)和所有权的转移(move),下面是一个稍微复杂点的:
fn main() {
let s = String::from("hello");
myFunc(s); // 字符串的所有权转移到了 myFunc() 的内部
let a = s; // error s 已经无法被使用
}
fn myFunc(s: String) {
println!("{}", s); // s 将在离开作用域时被释放
}
精通 JavaScript 的同学一定会在这里提出疑问:如果有闭包场景怎么处理所有权和内存回收?这里可以读一下这篇文章,解释的非常详细:《Rust 中的闭包与关键字 move》
所有权借用
有时变量值在作为函数参数使用后,在当前作用域仍要使用,函数结尾将其 return 是一个解决办法但不是好办法,这里 Rust 通过借用(borrow)来解决,先看一个例子:
fn main() {
let s = String::from("hello");
let a = &s;
println!("{}", s); // success
println!("{}", a); // success, print "hello"
func(&s); // success
}
fn func(s: &String) {
println!("{}", s);
}
变量 a
通过借用操作符 &
借用了 s
的内存,并没有转移,但现在 a
能访问 s
的空间,且允许有多个借用者,传入到函数 func()
的也是 s
的一个借用,会在 func()
结束时被释放。需要注意的是在被借用期间,拥有者不允许修改变量,或者转移所有权。
总结
通过上述所有权、转移、借用的机制,Rust 提供了一种全新的内存管理模型,它介于自动与手动之间,汲取了两者的优点,在编译期间处理内存管理,给后续其他编程语言有着很大的启发作用。
同时给大家分享一个 Rust 学习曲线,看看自己处于哪个阶段: