这一章我建议结合《Rust 语言圣经(Rust Course)》
这本书一起看
参考章节《Rust 程序设计语言》第15.6章 引用循环与内存泄漏
参考章节《Rust语言圣经(Rust Course)》第3.5章 循环引用与自引用
这一章主要是在讲内存泄漏
问题,如果你现在看不明白,暂时跳过
也没关系(因为就算你暂时不了解它,你平常应该也很难写出内存泄漏的代码)
虽然 Rust
并 不保证完全地避免内存泄漏
,但你仍然难以写出内存泄漏的代码,除非你故意的,因此你暂时
跳过这一章我认为也是可以的
这一章主要在阐述下面两个问题,了解它们,将对你写出更高质量
的代码有很大的帮助。
什么情况可能会发生内存泄漏
如何最大程度的避免内存泄漏
我们先来看看第一个问题 什么情况下会发生内存泄漏
以下代码,摘自《Rust 程序设计语言》
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
use crate::List::{Cons, Nil}; // 引入我们自定义的List中的 Cons 和 Nil 成员
use std::cell::RefCell;
use std::rc::Rc;
// 自定义枚举List
// Cons 成员代表有值的情况,其内部存储一个i32的值,和下一个List
// Nil 成员代表无值的情况
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>), // 第二个值存放下一个List,它既是可变的也是可克隆的
Nil,
}
impl List {
// tail 方法获取下一个List
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
|
我们来使用这个复杂的List
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
fn main() {
// 创建一个List a,它有 Cons 值 5,和指向下一个值是 Nil 的 List
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a的初始化rc计数 = {}", Rc::strong_count(&a)); // a = 1
println!("a指向的节点 = {:?}", a.tail()); // Nil List
// 创建一个List b,它有 Const 值 10,和指向下一个值是 a 的克隆的 List
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("在b创建后,a的rc计数 = {}", Rc::strong_count(&a)); // a = 2
println!("b的初始化rc计数 = {}", Rc::strong_count(&b)); // b = 1
println!("b指向的节点 = {:?}", b.tail()); // a List
// a.tail 取出了其 Cons 成员指向的 Nil List,利用可变性,把它改成 b 的克隆
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b); // 将 a 里面存储的 Nil List 改成 b
}
println!("在更改a后,b的rc计数 = {}", Rc::strong_count(&b)); // b = 2
println!("在更改a后,a的rc计数 = {}", Rc::strong_count(&a)); // a = 2
// 此时我们创建了循环引用 a -> b -> a ...
// 下面一行println!将导致内存泄漏,直至我们可怜的8MB大小的main线程栈空间将被它冲垮,最终造成栈溢出
// println!("a next item = {:?}", a.tail());
}
|
运行这个程序
1
2
3
4
5
6
7
|
a的初始化rc计数 = 1
a指向的节点 = Some(RefCell { value: Nil })
在b创建后,a的rc计数 = 2
b的初始化rc计数 = 1
b指向的节点 = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
在更改a后,b的rc计数 = 2
在更改a后,a的rc计数 = 2
|
可以看到 main
函数结束前,a
和 b
的引用计数均是 2
当 b
离开作用域时 b
触发 Drop
,此时引用计数会变为 1
,并不会归 0
,因此 b
指向的内存不会被释放
,同样的
当 a
离开作用域时 a
触发 Drop
,此时引用计数会变为 1
,也不会归 0
,因此 a
指向的内存也不会被释放
,最终发生了内存泄漏
。
当你取消最后一行的 println!
语句的注释时,由于 a -> b -> a
因此它会一直打印下去,直至栈溢出
下图展示了这种引用循环关系
下面这幅图是我画的,我觉得书上的图不好

因此总结一下
当你使用 RefCell<Rc<T>>
或者类似的类型嵌套组合
(具备内部可变性和引用计数)时,就要打起万分精神,前面可能是深渊!
我们再来看看第二个问题 如何最大程度的避免内存泄漏
- 可以使用
Weak
- 可以使用
unsafe
unsafe
我们现在没学到,所以就让我们来看看Weak
的使用
Weak
是一个弱化的 Rc
,它不增加引用计数器
,它不阻止对象的释放
(Rc
要等引用计数器=0
后才会释放,而Weak
不用)
它通过upgrade
方法返回一个Option<Rc<T>>
,当引用的对象不存在时返回None
,我们可以通过downgrade
方法产生一个Weak
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Weak<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Weak<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
fn main() {
let nil = Rc::new(Nil);
let a = Rc::new(Cons(5, RefCell::new(Rc::downgrade(&nil))));
println!("a的初始化rc计数 = {}", Rc::strong_count(&a)); // 1
println!("a指向的节点 = {:?}", a.tail()); // Weak -> Nil
let weak_a = Rc::downgrade(&a);
let b = Rc::new(Cons(10, RefCell::new(Weak::clone(&weak_a))));
println!("在b创建后,a的rc计数 = {}", Rc::strong_count(&a)); // 1
println!("b的初始化rc计数 = {}", Rc::strong_count(&b)); // 1
println!("b指向的节点 = {:?}", b.tail()); // Weak -> a
// 修改a指向b
let weak_b = Rc::downgrade(&b);
if let Some(link) = a.tail() {
*link.borrow_mut() = Weak::clone(&weak_b);
}
println!("在更改a后,b的rc计数 = {}", Rc::strong_count(&b)); // 1
println!("在更改a后,a的rc计数 = {}", Rc::strong_count(&a)); // 1
println!("a指向的节点 = {:?}", a.tail()); // Weak -> b
}
|
运行这个例子
1
2
3
4
5
6
7
8
|
a的初始化rc计数 = 1
a指向的节点 = Some(RefCell { value: (Weak) })
在b创建后,a的rc计数 = 1
b的初始化rc计数 = 1
b指向的节点 = Some(RefCell { value: (Weak) })
在更改a后,b的rc计数 = 1
在更改a后,a的rc计数 = 1
a指向的节点 = Some(RefCell { value: (Weak) })
|
从上面的例子可以看出引用计数器不会增加,被引用的数据在任何时候都可以释放,Weak不阻止释放,当引用数据无效时,Weak返回None
但我还没有理解到为什么 println! 打印到 Weak 就停了,而不会继续打印下去了?
这一点我还没有想清楚(不过我猜这个应该是println!
的机制问题)
- 当组合使用
RefCell
和 Rc
时要特别小心内存泄漏
问题
Weak
和 unsafe
可以解决内存泄漏