15.7 循环引用导致内存泄漏
15.7.1 内存泄漏
Rust 极高的安全性使得内存泄漏很难发生,但并非不可能。
例如,使用 Rc<T> 和 RefCell<T> 可能创造出引用循环并造成内存泄漏:每个指针的引用计数都不会减少到 0,因此这些值永远不会被清理。
看个例子:
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a initial rc count = {}", Rc::strong_count(&a));
println!("a next item = {:?}", a.tail());
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("a rc count after b creation = {}", Rc::strong_count(&a));
println!("b initial rc count = {}", Rc::strong_count(&b));
println!("b next item = {:?}", b.tail());
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
println!("b rc count after changing a = {}", Rc::strong_count(&b));
println!("a rc count after changing a = {}", Rc::strong_count(&a));
}
- 首先创建一个链表
List,用RefCell<T>包裹Rc<T>,以便可以修改内部值。 - 通过
impl块为List定义一个名为tail的方法,用于获取Cons变体携带的第二个元素。如果存在,就用Some包起来返回;如果是Nil,则返回None。 - 然后在
main中创建两个List实例a和b,并且b内部共享了a的值。这种链表代码看着很绕,所以我把结构图放在这里:
main还通过Rc::strong_count获取a和b的强引用计数,用自定义的tail方法获取Cons携带的第二个元素,并用println!打印出来。- 接下来,
if let语句把a的Cons的第二个值绑定到link上。它用borrow_mut获得对RefCell内Rc<List>的可变借用,再通过Rc::clone把b的克隆赋进去,从而把a的内部结构改成这样:
输出:
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2
- 第 1 行到第 5 行:刚创建
a时,引用计数为1。声明b时共享了a,因此a的引用计数变为2,b为1。 - 第 6 行到第 7 行:
if let语句改变了a的内部结构,使a的第二个元素指向b,于是b的引用计数增加到2。此时a指向b,b又指向a,就形成了引用循环。
当 a 和 b 都离开作用域时,Rust 丢弃变量 b,这会把 b 的引用计数从 2 减到 1。此时堆上的 Rc<List> 内存不会被删除,因为它的引用计数是 1 而不是 0。然后 Rust 丢弃 a,这会把 a 的 Rc<List> 实例的引用计数从 2 减到 1,如下所示。这个实例的内存也不能被删除,因为另一个 Rc<List> 实例仍然引用它。为列表分配的内存将永远保持未回收状态。
接下来,用下面这行代码看看循环里包含什么:
#![allow(unused)]
fn main() {
println!("a next item = {:?}", a.tail());
}
Rust 会尝试打印这个循环:a 指向 b,b 又指向 a,如此往复,直到栈溢出。最终结果会是栈溢出错误。
15.7.2 如何防止内存泄漏
那么有没有办法防止内存泄漏呢?这只能依靠开发者,不能只依赖 Rust。
否则,就需要重新组织数据结构,把引用拆分成持有所有权和不持有所有权的两类。一些引用用来表达所有权,一些引用不表达所有权。在引用循环中,一部分具有所有权关系,另一部分没有。这样,只有与所有权相关的链接才会影响值是否被清理。
15.7.3 把 Rc<T> 换成 Weak<T> 以防止循环
我们知道,Rc::clone 会创建数据的强引用,并使 Rc<T> 内部的引用计数加 1;只有当 strong_count 变为 0 时,Rc<T> 才会被清理。
然而,Rc<T> 实例可以通过调用 Rc::downgrade 来创建对某个值的弱引用。该方法的返回类型是 Weak<T>(也是智能指针)。每次调用 Rc::downgrade 会增加 weak_count 而不是 strong_count,因此弱引用不影响 Rc<T> 的清理。
15.7.4 Strong vs. Weak
强引用关乎如何分析 Rc<T> 实例的所有权。弱引用并不表达所有权,使用它也不会创建引用循环:当强引用计数变为 0 时,弱引用会自动断开。
在使用弱引用之前,需要确认它所指向的值仍然存在。在 Weak<T> 实例上调用 upgrade 方法会返回 Option<Rc<T>>,并通过 Option 枚举来验证该值是否存在。
看个例子:
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
}
Node 结构体表示一个节点,有两个字段:
value字段存储当前值,类型是i32。children字段存储子节点,类型是RefCell<Vec<Rc<Node>>>。这里使用Rc<T>是为了让所有子节点共享所有权。更具体地说,我们希望一个Node拥有它的子节点,同时也希望与保存该节点本身的变量共享这份所有权,以便可以直接访问树中的每个Node。为此,我们将Vec<T>的元素定义为Rc<Node>类型的值。
这里的需求是:每个节点既能指向父节点,也能指向子节点。
再看 main 函数:
leaf被创建为一个Node实例,value为3,children为被RefCell包裹的空Vector。branch被创建为一个Node实例,value为5,其children指向leaf。
这意味着 leaf 里面的 Node 有两个所有者。目前可以通过 branch 的 children 字段访问 leaf;但反过来还不能通过 leaf 访问 branch,所以还需要修改。
要实现这一点,就需要双向引用。但双向引用会创建引用循环,因此需要使用 Weak<T> 来避免循环:
#![allow(unused)]
fn main() {
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
}
我们添加一个 parent 字段表示父节点,并使用弱引用 Weak<T>。这里不用 Vec<>,因为这是树结构,一个节点只能有一个父节点。
要这样写,需要把 Weak<T> 引入作用域,并重构下面的代码。修改后的完整代码如下:
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
在创建 leaf 之后,先打印其 parent 字段的内容(此时 parent 还没有任何值)。创建 branch 之后,再打印一次 leaf 的 parent 字段内容(此时其值是 branch)。
语句 *leaf.parent.borrow_mut() = Rc::downgrade(&branch); 会创建一个指向 branch 的 Weak<Node>,并存入 leaf 的 parent 字段:
leaf.parent是表示leaf父节点的字段,类型是RefCell<Weak<Node>>,因此可以用borrow_mut得到RefMut<Weak<Node>>。- 解引用运算符
*让我们访问存储在RefMut<Weak<Node>>内部的Weak<Node>值。 downgrade方法从branch创建一个Weak<Node>,并赋给parent。
输出:
leaf parent = None
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) }, children: RefCell { value: [] } }] } })
- 第一次打印表明
parent字段尚未被赋值,因此其值是Option下的None变体。 - 第二次打印表明父节点已被设置为
branch;输出没有无限继续,说明这段代码没有创建引用循环。
最后,我们修改 main:添加打印语句并调整作用域,看看强引用和弱引用的数量:
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
{
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!(
"branch strong = {}, weak = {}",
Rc::strong_count(&branch),
Rc::weak_count(&branch),
);
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
代码逻辑是:
-
创建
leaf之后,打印它有多少强引用和弱引用。 -
之后加上
{}创建新作用域:- 把
branch的声明以及给leaf指定父节点的操作放进去。 - 打印此时
branch和leaf的强引用、弱引用数量。
- 把
-
离开作用域后:
- 打印
leaf的parent - 打印
leaf的强引用和弱引用
- 打印
输出:
leaf strong = 1, weak = 0
branch strong = 1, weak = 1
leaf strong = 2, weak = 0
leaf parent = None
leaf strong = 1, weak = 0
- 第 1 行:创建了
leaf,有一个强引用。 - 第 2 行:创建了
branch。在用Rc::downgrade(&branch)设置leaf.parent之后,branch有一个强引用和一个弱引用——这个弱引用计数来自leaf的parent字段,而不是来自branch自己那个空的Weak::new()。 - 第 3 行:
branch.children持有对leaf的强引用,而leaf变量本身也是一个强引用,因此此时leaf有两个强引用。 - 第 4 行:因为
branch已经离开作用域,所以leaf的parent字段现在是None。 - 第 5 行:
branch离开作用域导致它对leaf的强引用失效,于是leaf的强引用减1,变为1。