bigywater.tech
Rust · 110

Box、Rc、Arc 的内存布局

Rc 比 Box 多占多少内存?计数器放在哪里?Weak 为什么能在数据销毁后继续存在?这篇用 size_of、全局分配器的 Layout 记录、直接读堆内存和 Drop 计数,把 Box / Rc / Arc 的内存布局量出来。

24 字节
Rc / Arc 在堆上的分配大小(Box 是 8)
16 字节
Rc / Arc 前面的 strong + weak 计数器
1 次
Rc 从 &str 创建时的堆分配次数

指针本身

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    Box<u64> 8  Rc<u64> 8  Arc<u64> 8  &u64 8
    Box<dyn Debug> 16  Rc<dyn Debug> 16  Arc<dyn Debug> 16
    Box<[u8]> 16  Rc<str> 16  Arc<[u32]> 16  Box<str> 16
    Option<Box<u64>> 8  Option<Rc<u64>> 8  Option<Arc<u64>> 8
    Vec<u8> 24  String 24

Box 的基础见Box<T> 与堆分配。

堆块的大小

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    Box<u64>(8 字节的数据):       分配 8 字节,对齐 8,1 次分配
    Rc<u64>:                   分配 24 字节,对齐 8,1 次分配(8 字节数据 + 16 字节的计数器)
    Arc<u64>:                  分配 24 字节,对齐 8,1 次分配
    Rc<[u8; 100]>:             分配 120 字节(16 字节计数器 + 100 字节数据 = 116,按对齐 8 补到 120)
    Rc<str> from "hello":     分配 24 字节(16 字节计数器 + 5 字节字符串 = 21,补到 24),1 次分配(字符串与计数器在同一块)
    Box<()>(零大小类型):         0 次分配

计数器

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    Rc::new:     (strong, weak) = (1, 1)(weak 起始为 1:所有 strong 引用合起来持有的那一个隐式 weak)
    clone 一次:   (2, 1)  strong_count() = 2  weak_count() = 0
    downgrade:    (2, 2)  strong_count() = 2  weak_count() = 1
    drop 两个 Rc: Weak::upgrade() = None(数据已被 drop,但内存块还在,因为还有 Weak)
    Arc 同样: (2, 1)  strong_count = 2

Rc 的行为见Rc<T>,Weak 见Weak<T>。

数据与内存块的释放时机

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    (先打印一行,让 stdout 的缓冲区先分配好,以免干扰计数)
    (Noisy::drop 被调用:数据被销毁)
    创建 Rc + Weak 后:        存活的堆块 +1
    drop 最后一个强引用后:   存活的堆块 +1(上面打印的 Noisy::drop 说明数据已销毁,内存块仍在,因为 Weak 还指着它)
    drop 最后一个 Weak 后:   存活的堆块 +0

分步演示

Box、Rc、Arc 布局
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。偷看计数器用的是 unsafe 指针读取,依赖当前标准库的内存布局(RcBox 的字段顺序),不是稳定保证
    • 没有验证:Arc 的原子计数器的内存序、Rc 的块布局(vtable 在胖指针里,没有在堆块里)、Box 的 drop 路径、其它平台的大小与对齐
    京ICP备2026067502号-1京公网安备11010502063487号