正式开始
内存
// 1. “hello world” 作为一个字符串常量(string literal),在编译时被存入可执行文件的 .RODATA 段(GCC)或者 .RDATA 段(VC++),然后在程序加载时,获得一个固定的内存地址
// 2. 当执行 “hello world”.to_string() 时,在堆上,一块新的内存被分配出来,并把 “hello world” 逐个字节拷贝过去。
// 3. 当我们把堆上的数据赋值给 s 时,s 作为分配在栈上的一个变量.使用了三个 word:第一个表示指针、第二个表示字符串的当前长度(11)、第三个表示这片内存的总容量(11)。在 64 位系统下,三个 word 是 24 个字节
let s = "hello world".to_string();
println!("addr of ss: {:p}, s: {:p}, len: {}, capacity: {}, size: {}",
&"hello world", &s, s.len(), s.capacity(), std::mem::size_of_val(&s));
// addr of ss: 0x5626bde6b1f0, s: 0x7ffc83481168, len: 11, capacity: 11, size: 24
栈
栈是程序运行的基础。每当一个函数被调用时,一块连续的内存就会在栈顶被分配出来,这块内存被称为帧(frame) 在编译时,一切无法确定大小或者大小可以改变的数据,都无法安全地放在栈上,最好放在堆上
- 栈是自顶向下增长的,一个程序的调用栈最底部,除去入口帧(entry frame),就是 main() 函数对应的帧,而随着 main() 函数一层层调用,栈会一层层扩展;调用结束,栈又会一层层回溯,把内存释放回去
- 在调用的过程中,一个新的帧会分配足够的空间存储寄存器的上下文。在函数里使用到的通用寄存器会在栈保存一个副本,当这个函数调用结束,通过副本,可以恢复出原本的寄存器的上下文,就像什么都没有经历一样。此外,函数所需要使用到的局部变量,也都会在帧分配的时候被预留出来
- 函数编译并优化代码的最小单元
- 栈空间的调整是通过移动栈指针的方式来进行的
总结
对于存入栈上的值,它的大小在编译期就需要确定。栈上存储的变量生命周期在当前调用栈的作用域内,无法跨调用栈引用
- FP 是 frame pointer,指向栈帧的起始位置。
- SP(stack pointer)指向栈顶,它会随着栈的操作而变化。
- 在开一个新的栈帧的时候,在栈顶会压入 callee 函数的参数,caller 返回地址,以及 caller FP
堆
堆上分配内存时,一般都会预留一些空间,这是最佳实践
分配在堆内存上的数据: 1、动态大小的内存; 2、动态生命周期的内存;
- 堆上内存分配会使用 libc 提供的 malloc() 函数,其内部会请求操作系统的系统调用,来分配内存。系统调用的代价是昂贵的,所以要避免频繁地 malloc()。
堆上的内存有一些问题
- 内存泄漏
- 堆越界(多线程访问共享资源)
- 野指针问题
总结
堆可以存入大小未知或者动态伸缩的数据类型
堆栈总结
栈上存放的数据是静态的,固定大小,固定生命周期;
堆上存放的数据是动态的,不固定大小,不固定生命周期
新鲜知识点
数据的存放方式会严重影响并发安全
追踪式垃圾回收
通过定期标记(mark)找出不再被引用的对象,然后将其清理(sweep)掉,来自动管理内存
GC效率高,但是会存在STW(Stop The World)问题。但Erlang VM把 GC 的粒度下放到每个 process,最大程度解决了 STW 的问题
自动引用计数内存管理
在编译时,它为每个函数插入 retain/release 语句来自动维护堆上对象的引用计数,当引用计数为零的时候,release 语句就释放对象
精选问答
-
"{:p}"输出某个变量的地址,是这个变量自身结构的地址,还是变量所指向值的地址呢
a. String 在 Rust 中是一个智能指针,它内部是一个结构体,放在栈上,结构体中有指针指向堆内存。所以 &s 指向一个栈上的地址
b. {:p} 是通过 Pointer trait 实现。请移步 Pointer Trait 及地址验证
static MAX: u32 = 0;
fn foo() {}
fn main() {
let hello = "hello world".to_string();
let data = Box::new(1);
// string literals 指向 RODATA 地址
println!("RODATA: {:p}", "hello world!");
// static 变量在 DATA section
println!("DATA (static var): {:p}", &MAX);
// function 在 TEXT
println!("TEXT (function): {:p}", foo as *const ());
// String 结构体分配在栈上,所以其引用指向一个栈地址
println!("STACK (&hello): {:p}", &hello);
// 需要通过解引用获取其堆上数据,然后取其引用
println!("HEAP (&*hello): {:p}", &*hello);
// Box 实现了 Pointer trait 无需额外解引用
println!("HEAP (box impl Pointer) {:p} {:p}", data, &*data);
}
/*
RODATA: 0x55c793ad915c
DATA (static var): 0x55c793ad9144
TEXT (function): 0x55c793aa3fa0
STACK (&hello): 0x7ffd0b1eef78
HEAP (&*hello): 0x55c793f4e9d0
HEAP (box impl Pointer) 0x55c793f4e9f0 0x55c793f4e9f0
*/
-
如果有一个数据结构需要在多个线程中访问,可以把它放在栈上吗?为什么?
a. 不能,栈上的数据会随着当前线程的函数调用栈而回收,多个线程访问须在堆上开辟。
b. 在多线程场景下,每个线程的生命周期是不固定的,无法在编译期知道谁先结束谁后结束,所以不能把属于某个线程 A 调用栈上的内存共享给线程 B,因为 A 可能先于 B 结束。这时候,只能使用堆内存。这里有个例外,如果结束的顺序是确定的,那么可以共享,比如 scoped thread;
-
可以使用指针引用栈上的某个变量吗?如果可以,在什么情况下可以这么做?
a. 可以,在当前函数调用栈中,可以新建变量在栈上开辟,顺便分配一个指针指向它,但是注意,这个指针的生命周期只能在当前栈帧中,不能作为返回值给别人用。
b. 同一个调用栈下,main() 调用 hello(),再调用 world(),编译器很清楚,world() 会先结束,之后是 hello(),最后是 main()。所以在 world() 下用指针引用 hello() 或者 main() 内部的变量没有问题,这个指针必然先于它指向的值结束。
-
怎么理解 在编译时,一切无法确定大小或者大小可以改变的数据,都无法安全地放在栈上,最好放在堆上
a. 可变参数的函数是一个很好的例子。对于 Java,可变参数 String... a 是 String[] 的语法糖,它是放在堆上的。在 C 语言里,这个行为是未定义的,它只是定义了你可以通过 var_start / var_end 来获得可变参数的起始位置,以及最终结束可变参数的访问。但 gcc 的实现将可变参数放在栈上(估计是为了性能)
b. alloca() 可以在栈上分配动态大小的内存,然而使用它需要非常小心,按 linux 的文档(man7.org/linux/man-p… longjmp 使用。alloca() 如果分配太大的数据,超过栈容量会导致程序崩溃,即使你分配很小的数据,但如果使用 alloca() 的函数被优化导致 inline,又恰巧出现在大的 for/while 循环中,也可能会导致崩溃
所以,这两种在栈上分配可变大小的数据,是不安全的
-
可执行文件加载string literal 到内存,如果不同的两个函数,foo() 和bar()中,各自都用到了一个string literal “hello”, 那么编译器从从可执行文件.rodata 中加载内存,是加载一份”hello”,还是两份?
是一份。它们指向 .rodata 中同样的地址
-
如何确定当前开发环境里调用栈的最大大小
在 *nix 下,你可以通过 ulimit -s 来查看和修改。linux 和 osx 默认是 8M