跳转至

循环引用导致内存泄漏

一、内存泄漏的示例

rust的内存安全机制可以保证很难发生内存泄漏,但不是不可能。如使用Rc<T>RefCell<T>就可能造出循环使用,从而发生内存泄漏。因为每项的引用数量不会变成0,值也不会被处理掉。我们先看下面的示例代码

use std::{cell::RefCell, rc::Rc};
use crate::List::{Cons, Nil};

#[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))));

    // 打印a的强引用
    println!("a initial rc count = {}", Rc::strong_count(&a));
    // 打印a的第二个元素
    println!("a text item = {:?}", a.tail());

    // b的第一个元素是10,第二个元素共享a的数据
    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
    // 打印a的强引用
    println!("a rc count after b creation = {}", Rc::strong_count(&a));
    // 打印b的强引用
    println!("b initial rc count = {}", Rc::strong_count(&b));
    // 打印b的第二个元素
    println!("b next item = {:?}", b.tail());

    // 取出a的第二个元素
    if let  Some(link) = a.tail() {
        // 把a原来存储的Nil改为B存储的值
        *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));
}

该开始创建a的时候,它的引用数量是1,它的第二个元素是2,b创建之后,a的强引用个数变成2,b的引用个数变成1,b的第二个元素是a。接下来让a的第二个元素指向b,所以这个时候b有两个引用,a也有两个引用,相当于此时创建了一个循环的数据结构,如下图

54-01

a的第一个元素是5,第二个元素是b,b的第一个元素是10,第二个元素是a。这时候形成了一个循环。假如此时代码执行完毕,将先释放b,因为b是在后面创建的。程序将会把b对应的数据的引用计数减少到1,因为此时a仍然存在一个指向b对应的数据的引用。那么b对应的List在堆内存上讲不会进行释放,如果我们在main函数的最后添加以下一行代码,将会导致堆栈溢出

println("a next item = {:?}", a.tail());

a的第二个元素是b,而的第二个元素是a,以此循环,最终导致堆栈溢出。所以,在rust里,内存溢出并不容易,但绝非不可能。

二、防止内存泄漏的解决办法

2.1 依靠开发者来保证,不能依靠rust

如认证检查自己的代码逻辑,并做一些测试,防止内存泄漏

2.2 重组数据结构

简单来说把引用拆分来持有所有权和不持有所有权两种情况,如在循环引用中,让某些指向关系具有所有权,而另外一部分的指向关系不涉及所有权。这样只有持有所有权的关系才影响值的清理。

2.3 防止循环引用,把Rc换成Weak

Rc::cloneRc<T>示例的strong_count加1,Rc<T>的实例只有在strong_count为0的时候才会被清理。

Rc<T>实例通过调用Rc::downgrade方法可以创建值的Weak Reference(弱引用),返回值类型是Weak<T>(只能指针)。每次调用Rc::downgrade方法会为weak_count(即弱引用计数)加1,而Rc<T>使用weak_count来追踪存在多少Weak<T>weak_count不为0并不影响Rc<T>实例的清理。

2.4 Strong VS Weak

  • Strong Reference(强引用)是关于如何分享Rc<T>实例的所有权
  • Weak Reference(弱引用)并不表述上述意思

使用Weak Reference并不会创建循环引用,因为当Strong Reference数量为0的时候,Weak Reference会自动断开。在使用Weak<T>前,需保证它指向的值仍然存在,在Weak<T>实例上调用upgrade方法,返回Option<Rc<T>>

2.5 Weak Reference使用示例

我们先看以下的示例

use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
// 定义树的节点
struct Node {
    value: i32,
    // 使用Rc<Node>为了让所有的子节点能够共享所有权
    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)]),
    });
}

上面示例代码中,我们可以通过树枝找到叶子,但还不可以通过叶子找到树枝,因为树叶还不持有树枝的引用,它对它们两之间的父子关系一无所知。所以我门需要修改一下代码,但是双向的引用会是循环引用,这个时候我们就可以使用Weak<T>,让它避免产生循环引用。修改代码如下

use std::borrow::Borrow;
use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[derive(Debug)]
// 定义树的节点
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    // 使用Rc<Node>为了让所有的子节点能够共享所有权
    children: RefCell<Vec<Rc<Node>>>
}

fn main() {
    // 创建一个叶子
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    // 通过树叶找到树枝,通过borrow()获得不可变引用,在通过upgrade()将Weak<T>转换为Rc<T>
    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)]),
    });

    // 让树叶的parent指向树枝
    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

为了观察数据强引用也弱引用的计数变化,我们修改代码如下

use std::borrow::Borrow;
use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
// 定义树的节点
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    // 使用Rc<Node>为了让所有的子节点能够共享所有权
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    // 创建一个叶子
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });
    // 打印leaf
    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)]),
        });

        // 让树叶的parent指向树枝
        *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

        // 打印branch
        println!(
            "branch strong = {}, weak = {}",
            Rc::strong_count(&branch),
            Rc::weak_count(&branch)
        );

        // 打印leaf
        println!(
            "leaf strong = {}, weak = {}",
            Rc::strong_count(&leaf),
            Rc::weak_count(&leaf)
        );
    }

    // 使用leaf访问branch
    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
    // 打印leaf
    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf)
    );

}

运行结果如下

leaf strong = 1, weak = 0
branch strong = 1, weak = 1
leaf strong = 2, weak = 0
leaf parent = None
leaf strong = 1, weak = 0

根据结果,我们得出一下结论:

  • 创建leaf之后,有1个强引用,0个弱引用。

  • 然后进入一个作用域,在作用域里创建branch,再把leaf的parent连接到branchbranch有1个强引用,并且多了1个弱引用,因为leaf对其做了关联。这时候leaf有2个强引用,因为在创建branch的时候,branch中的children对其有一个强引用。

  • 最后走出作用域,通过leaf访问它的parent,得到的结果是None。此时leaf剩下本省1个强引用,而弱引用也是0。

所以看一看到,branch离开作用域之后,它的强引用变成0,此时相当于被丢弃了。虽然在branch的生命周期内它拥有一个弱引用,但并不影响其数据的销毁。