Reference Cycles
A reference cycle occurs when two or more reference-counted allocations (Rc<T> or Arc<T>) hold strong references to one another through interior mutability (RefCell<T>, Mutex<T>, etc.), forming a closed loop. Because each node in the cycle keeps the other node's strong_count at 1 or higher, dropping all external stack handles leaves the cycle's strong_count above zero, leaking the heap memory and skipping Drop destructors.
How a Reference Cycle Leaks Memory
Memory leaks caused by reference cycles are memory-safe (they never cause dangling pointers or undefined behavior), which is why Rust allows Rc<RefCell<T>> in safe code. However, any resources held inside the cycle remain allocated for the rest of the process lifetime.
Stack Handles Dropped:
(a dropped) (b dropped)
Heap Cycle Remains (strong_count == 1 on both nodes forever):
+-----------------------+ +-----------------------+
| Node A | next | Node B |
| strong_count = 1 +----------> | strong_count = 1 |
| | <----------+ |
+-----------------------+ next +-----------------------+
use std::cell::RefCell;
use std::rc::Rc;
struct CyclicNode {
label: &'static str,
next: RefCell<Option<Rc<CyclicNode>>>,
}
impl Drop for CyclicNode {
fn drop(&mut self) {
println!("Dropping CyclicNode {}", self.label);
}
}
fn main() {
let a = Rc::new(CyclicNode {
label: "A",
next: RefCell::new(None),
});
let b = Rc::new(CyclicNode {
label: "B",
next: RefCell::new(Some(Rc::clone(&a))),
});
// Close the cycle: A -> B and B -> A
*a.next.borrow_mut() = Some(Rc::clone(&b));
println!("before scope exit -> a strong: {}, b strong: {}", Rc::strong_count(&a), Rc::strong_count(&b));
// When `main` ends, `a` and `b` decrement strong_count from 2 to 1.
// Neither node's `Drop` implementation ever runs!
}
before scope exit -> a strong: 2, b strong: 2
Notice that "Dropping CyclicNode A" and "Dropping CyclicNode B" are never printed.
Breaking Reference Cycles with Weak<T>
To prevent reference cycles in trees and graphs, distinguish ownership edges from non-owning back-edges:
- Parent $\rightarrow$ Child: A parent owns its children; if the parent is dropped, its children should also be dropped (
Vec<Rc<Node>>). - Child $\rightarrow$ Parent: A child does not own its parent; if a child is dropped, the parent must remain alive, and if the parent is dropped, the child's back-pointer must not keep the parent alive (
RefCell<Weak<Node>>).
Parent-Child Hierarchy Using Strong Downward Edges and Weak Upward Edges:
+-----------------------------------+
| Parent Node |
| children: Vec<Rc<Node>> |
+---------+---------------+---------+
| ^
Strong Rc<Node> | | Weak<Node> (does not increment strong_count)
(owns child) v |
+---------+---------------+---------+
| Child Node |
| parent: RefCell<Weak<Node>> |
+-----------------------------------+
use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct TreeNode {
name: &'static str,
parent: RefCell<Weak<TreeNode>>,
children: RefCell<Vec<Rc<TreeNode>>>,
}
impl Drop for TreeNode {
fn drop(&mut self) {
println!("Dropping TreeNode {}", self.name);
}
}
fn main() {
let leaf = Rc::new(TreeNode {
name: "leaf",
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
{
let branch = Rc::new(TreeNode {
name: "branch",
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!(
"inside scope -> branch strong: {}, weak: {}",
Rc::strong_count(&branch),
Rc::weak_count(&branch)
);
println!(
"leaf parent name: {:?}",
leaf.parent.borrow().upgrade().map(|p| p.name)
);
} // `branch` drops here because its strong_count is 1!
println!(
"after branch scope -> leaf parent: {:?}",
leaf.parent.borrow().upgrade().map(|p| p.name)
);
}
inside scope -> branch strong: 1, weak: 1
leaf parent name: Some("branch")
Dropping TreeNode branch
after branch scope -> leaf parent: None
Dropping TreeNode leaf
Alternative for Arbitrary Cyclic Graphs: Arena / Index Graphs
When a graph has arbitrary peer-to-peer cycles with no natural parent-child hierarchy (such as a state machine, control-flow graph, or social network), storing nodes in a contiguous Vec<Node> (or generational arena) and referencing neighbors by integer index (usize) avoids reference counting altogether and frees the entire graph when the Vec<Node> drops.