Skip to content

搞英语 → 看世界

翻译英文优质信息和名人推特

Menu
  • 首页
  • 作者列表
  • 独立博客
  • 专业媒体
  • 名人推特
  • 邮件列表
  • 关于本站
Menu

理解 Rust 的内存模型

Posted on 2025-07-08

所以我主要从事垃圾收集编程语言的工作。我写过不少 C++ 代码,但那是在大学期间,当时我做 LeetCode 题目和大学作业。最近我开始学习 Rust,以下是我对其抽象底层原理的理解。

你可以把这篇文章看作一篇博客,我会引导你了解什么是内存,以及 Rust 中内存是如何分配的。以上就是我的理解,如果你觉得我写得有误或者有需要改进的地方,请在X/Twitter上私信我。

在此之前,WTF 是什么?

好吧,把内存想象成一个巨大的数组,每个元素可以容纳 8 位数据。这些数据就是一个地址。

 Address | Value (in binary) ---------|------------------ 0x1000 | 01110011 (115 in decimal, 's' in ASCII) 0x1001 | 01101100 (108 in decimal, 'l' in ASCII) 0x1002 | 01101111 (111 in decimal, 'o' in ASCII) 0x1003 | 01110000 (112 in decimal, 'p' in ASCII)堆栈:快速且有序Address | Value (in binary) ---------|------------------ 0x1000 | 01110011 (115 in decimal, 's' in ASCII) 0x1001 | 01101100 (108 in decimal, 'l' in ASCII) 0x1002 | 01101111 (111 in decimal, 'o' in ASCII) 0x1003 | 01110000 (112 in decimal, 'p' in ASCII)

栈的工作原理就像你只能从栈顶添加/推送或移除/弹出元素一样。它非常快,因为 CPU 只需要上下移动指针。

当我们在 Rust 中创建一个变量时(如下所示),所有变量都进入堆栈内存:

 fn main() { let x: i32 = 100; let flag: bool = true; let point: f64 = 0.5; }内存如下所示: fn main() { let x: i32 = 100; let flag: bool = true; let point: f64 = 0.5; }

 Stack +---------------------+ <-- Top of Stack (higher address) | point = 0.5 | f64 (8 bytes) +---------------------+ | flag = true | bool (1 byte + padding) +---------------------+ | x = 100 | i32 (4 bytes) +---------------------+ <-- Bottom (lowest address for this frame)这里需要注意以下几点: Stack +---------------------+ <-- Top of Stack (higher address) | point = 0.5 | f64 (8 bytes) +---------------------+ | flag = true | bool (1 byte + padding) +---------------------+ | x = 100 | i32 (4 bytes) +---------------------+ <-- Bottom (lowest address for this frame)

  • 每个变量都是按照我们声明的方式从上到下存储的,但堆栈向下增长,因此point最后被推送,但出现在内存的顶部
  • Rust 编译器会针对性能优化布局,因此实际的内存布局可能由于填充而导致变量之间出现间隙
  • 关于堆栈分配:
    • 编译时必须知道大小
    • 当变量超出范围时,堆栈会被清理

堆:速度较慢但空间较大

堆是一个动态内存区域,你可以在运行时申请空间。它不是直接存储数据,而是指向分配的内存的指针。

 fn main() { let v = vec![65, 54, 57]; let s = String::from("slop"); } Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ s: String │ │ "slop" (4B) │ ← 0x5000 │ ptr: 0x5000 ───┼───────┤ │ │ len: 5 │ └─────────────────┘ │ capacity: 5 │ ├─────────────────┤ ┌────────────────────┐ │ v: Vec<i32> │ │ 65 │ 54 │ 57 │ │ ← 0x4000 │ ptr: 0x4000 ───┼───────┤ │ │ │ │ │ len: 3 │ └────┴────┴────┴─────┘ │ capacity: 3 │ └─────────────────┘ fn main() { let v = vec![65, 54, 57]; let s = String::from("slop"); }
 Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ s: String │ │ "slop" (4B) │ ← 0x5000 │ ptr: 0x5000 ───┼───────┤ │ │ len: 5 │ └─────────────────┘ │ capacity: 5 │ ├─────────────────┤ ┌────────────────────┐ │ v: Vec<i32> │ │ 65 │ 54 │ 57 │ │ ← 0x4000 │ ptr: 0x4000 ───┼───────┤ │ │ │ │ │ len: 3 │ └────┴────┴────┴─────┘ │ capacity: 3 │ └─────────────────┘所以这里要注意一件事,堆栈有一个指针“ptr”,指向堆上的实际数据。 Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ s: String │ │ "slop" (4B) │ ← 0x5000 │ ptr: 0x5000 ───┼───────┤ │ │ len: 5 │ └─────────────────┘ │ capacity: 5 │ ├─────────────────┤ ┌────────────────────┐ │ v: Vec<i32> │ │ 65 │ 54 │ 57 │ │ ← 0x4000 │ ptr: 0x4000 ───┼───────┤ │ │ │ │ │ len: 3 │ └────┴────┴────┴─────┘ │ capacity: 3 │ └─────────────────┘

所有权与记忆

让我们举个例子:

 let x = String::from("x"); let y = x;在let x = String::from("x"); let y = x;

let y=x;内存布局如下:

 Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ x: String │ │ "x" (1B) │ │ ptr: 0x1000 ───┼───────┤ │ │ len: 1 │ └─────────────────┘ │ capacity: 1 │ └─────────────────┘后Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ x: String │ │ "x" (1B) │ │ ptr: 0x1000 ───┼───────┤ │ │ len: 1 │ └─────────────────┘ │ capacity: 1 │ └─────────────────┘

let y = x;发生的事情是 x 被移动到 y 所以:

 Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ x: (invalid) │ │ "x" (1B) │ ├─────────────────┤ │ │ │ y: String │ │ │ │ ptr: 0x1000 ───┼───────┤ │ │ len: 1 │ └─────────────────┘ │ capacity: 1 │ └─────────────────┘为什么 x 无效!?!? Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ x: (invalid) │ │ "x" (1B) │ ├─────────────────┤ │ │ │ y: String │ │ │ │ ptr: 0x1000 ───┼───────┤ │ │ len: 1 │ └─────────────────┘ │ capacity: 1 │ └─────────────────┘

Rust 编译器使x无效以防止:

  • 双重释放:x 和 y 都试图释放同一块内存
  • 释放后使用:在 y 释放内存后使用 x

借用与记忆

让我们再举一个例子(这里 y 将借用 x):

 let x = String::from("tpot"); let y = &x; Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ x: String │ │ "tpot" (4B) │ ← 0x5000 │ ptr: 0x5000 ───┼───────┤ │ │ len: 4 │ └─────────────────┘ │ capacity: 4 │ ├─────────────────┤ │ y: &String │ │ ptr: &x │ └─────────────────┘ let x = String::from("tpot"); let y = &x;
 Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ x: String │ │ "tpot" (4B) │ ← 0x5000 │ ptr: 0x5000 ───┼───────┤ │ │ len: 4 │ └─────────────────┘ │ capacity: 4 │ ├─────────────────┤ │ y: &String │ │ ptr: &x │ └─────────────────┘什么是参考文献? Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ x: String │ │ "tpot" (4B) │ ← 0x5000 │ ptr: 0x5000 ───┼───────┤ │ │ len: 4 │ └─────────────────┘ │ capacity: 4 │ ├─────────────────┤ │ y: &String │ │ ptr: &x │ └─────────────────┘

  • 他们不拥有数据
  • 它们不会比它们所引用的数据存在得更久

Box:显式堆分配

Box 是一个智能指针,可以让你明确地将值放在堆上

Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ b: Box<i32> │ │ 5 (4 bytes) │ ← 0x6000 │ ptr: 0x6000 ───┼───────┤ │ └─────────────────┘ └─────────────────┘ let b = Box::new(5);
 Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ b: Box<i32> │ │ 5 (4 bytes) │ ← 0x6000 │ ptr: 0x6000 ───┼───────┤ │ └─────────────────┘ └─────────────────┘ Rust 中的复制Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ b: Box<i32> │ │ 5 (4 bytes) │ ← 0x6000 │ ptr: 0x6000 ───┼───────┤ │ └─────────────────┘ └─────────────────┘

某些类型实现了Copy特性,因此会被复制而不是移动

let x = 100; let y = x; // x & y both a valid cause y is an independent copy实现的类型let x = 100; let y = x; // x & y both a valid cause y is an independent copy

Copy (可能还有更多,请核对):

  • 整数类型
  • 浮点类型
  • 布尔值
  • 字符
  • 复制类型的元组
  • 复制类型的数组

你注意到了一件事!?只有在Stack上分配的数据类型才会实现Copy特性。这是我注意到的,可能是 true 也可能是 false。

Rust 中的内存泄漏

但是 Rust 是内存安全的,对吧!?没错,Rust 可以防止user after free和double-free ,但我们仍然可以使用Rc<T>和RefCell<T>来创建循环引用。

 struct Node { next: Option<Rc<RefCell<Node>>>, }在上面的代码中,它创建了一个不会被释放的循环。当两个struct Node { next: Option<Rc<RefCell<Node>>>, }

Rc指针相互循环引用,它们的引用计数永远不会达到零,这意味着内存永远不会被释放。这是为什么呢?因为Rc使用引用计数,为了打破循环,它使用Weak<T>来建立单向关系,这在强引用被移除时不会阻止内存释放。

TLDR 😉

  • 堆栈:快速、固定大小、自动清理、后进先出(LIFO)
  • 堆:大小灵活,需要分配/释放,速度较慢
  • 所有权:每个值只有一个所有者
  • 移动:转移所有权,防止双重释放
  • 借用:临时使用但不授予所有权

原文: https://minami.bearblog.dev/rust-memory-model/

本站文章系自动翻译,站长会周期检查,如果有不当内容,请点此留言,非常感谢。
  • Abhinav
  • Abigail Pain
  • Adam Fortuna
  • Alberto Gallego
  • Alex Wlchan
  • Answer.AI
  • Arne Bahlo
  • Ben Carlson
  • Ben Kuhn
  • Bert Hubert
  • Bits about Money
  • Brian Krebs
  • ByteByteGo
  • Chip Huyen
  • Chips and Cheese
  • Christopher Butler
  • Colin Percival
  • Cool Infographics
  • Dan Sinker
  • David Walsh
  • Dmitry Dolzhenko
  • Dustin Curtis
  • eighty twenty
  • Elad Gil
  • Ellie Huxtable
  • Ethan Dalool
  • Ethan Marcotte
  • Exponential View
  • FAIL Blog
  • Founder Weekly
  • Geoffrey Huntley
  • Geoffrey Litt
  • Greg Mankiw
  • Henrique Dias
  • Hypercritical
  • IEEE Spectrum
  • Investment Talk
  • Jaz
  • Jeff Geerling
  • Jonas Hietala
  • Josh Comeau
  • Lenny Rachitsky
  • Liz Danzico
  • Lou Plummer
  • Luke Wroblewski
  • Matt Baer
  • Matt Stoller
  • Matthias Endler
  • Mert Bulan
  • Mind Matters
  • Mostly metrics
  • Naval Ravikant
  • News Letter
  • NextDraft
  • Non_Interactive
  • Not Boring
  • One Useful Thing
  • Phil Eaton
  • Product Market Fit
  • Readwise
  • ReedyBear
  • Robert Heaton
  • Rohit Patel
  • Ruben Schade
  • Sage Economics
  • Sam Altman
  • Sam Rose
  • selfh.st
  • Shtetl-Optimized
  • Simon schreibt
  • Slashdot
  • Small Good Things
  • Steve Blank
  • Taylor Troesh
  • Telegram Blog
  • The Macro Compass
  • The Pomp Letter
  • thesephist
  • Thinking Deep & Wide
  • Tim Kellogg
  • Understanding AI
  • Wes Kao
  • 英文媒体
  • 英文推特
  • 英文独立博客
©2025 搞英语 → 看世界 | Design: Newspaperly WordPress Theme