01|内存:值放堆上还是放栈上,这是一个问题

765 阅读7分钟

正式开始

RustPlayground HelloWorld 代码

内存

// 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

image.png

栈是程序运行的基础。每当一个函数被调用时,一块连续的内存就会在栈顶被分配出来,这块内存被称为帧(frame) 在编译时,一切无法确定大小或者大小可以改变的数据,都无法安全地放在栈上,最好放在堆上

  1. 栈是自顶向下增长的,一个程序的调用栈最底部,除去入口帧(entry frame),就是 main() 函数对应的帧,而随着 main() 函数一层层调用,栈会一层层扩展;调用结束,栈又会一层层回溯,把内存释放回去
  2. 在调用的过程中,一个新的帧会分配足够的空间存储寄存器的上下文。在函数里使用到的通用寄存器会在栈保存一个副本,当这个函数调用结束,通过副本,可以恢复出原本的寄存器的上下文,就像什么都没有经历一样。此外,函数所需要使用到的局部变量,也都会在帧分配的时候被预留出来
  3. 函数编译并优化代码的最小单元
  4. 栈空间的调整是通过移动栈指针的方式来进行的

总结

对于存入栈上的值,它的大小在编译期就需要确定。栈上存储的变量生命周期在当前调用栈的作用域内,无法跨调用栈引用

image.png

  1. FP 是 frame pointer,指向栈帧的起始位置。
  2. SP(stack pointer)指向栈顶,它会随着栈的操作而变化。
  3. 在开一个新的栈帧的时候,在栈顶会压入 callee 函数的参数,caller 返回地址,以及 caller FP

堆上分配内存时,一般都会预留一些空间,这是最佳实践

分配在堆内存上的数据: 1、动态大小的内存; 2、动态生命周期的内存;

  1. 堆上内存分配会使用 libc 提供的 malloc() 函数,其内部会请求操作系统的系统调用,来分配内存。系统调用的代价是昂贵的,所以要避免频繁地 malloc()。

image.png 堆上的内存有一些问题

  1. 内存泄漏
  2. 堆越界(多线程访问共享资源)
  3. 野指针问题

总结

堆可以存入大小未知或者动态伸缩的数据类型

堆栈总结

栈上存放的数据是静态的,固定大小,固定生命周期;

堆上存放的数据是动态的,不固定大小,不固定生命周期

新鲜知识点

数据的存放方式会严重影响并发安全

微软安全反应中心(MSRC)的研究

追踪式垃圾回收

通过定期标记(mark)找出不再被引用的对象,然后将其清理(sweep)掉,来自动管理内存

GC效率高,但是会存在STW(Stop The World)问题。但Erlang VM把 GC 的粒度下放到每个 process,最大程度解决了 STW 的问题

自动引用计数内存管理

在编译时,它为每个函数插入 retain/release 语句来自动维护堆上对象的引用计数,当引用计数为零的时候,release 语句就释放对象

Golang STW 从1次10ms优化到两次500us

精选问答

  1. "{: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
*/
  1. 如果有一个数据结构需要在多个线程中访问,可以把它放在栈上吗?为什么?

    a. 不能,栈上的数据会随着当前线程的函数调用栈而回收,多个线程访问须在堆上开辟。

    b. 在多线程场景下,每个线程的生命周期是不固定的,无法在编译期知道谁先结束谁后结束,所以不能把属于某个线程 A 调用栈上的内存共享给线程 B,因为 A 可能先于 B 结束。这时候,只能使用堆内存。这里有个例外,如果结束的顺序是确定的,那么可以共享,比如 scoped thread;

  2. 可以使用指针引用栈上的某个变量吗?如果可以,在什么情况下可以这么做?

    a. 可以,在当前函数调用栈中,可以新建变量在栈上开辟,顺便分配一个指针指向它,但是注意,这个指针的生命周期只能在当前栈帧中,不能作为返回值给别人用。

    b. 同一个调用栈下,main() 调用 hello(),再调用 world(),编译器很清楚,world() 会先结束,之后是 hello(),最后是 main()。所以在 world() 下用指针引用 hello() 或者 main() 内部的变量没有问题,这个指针必然先于它指向的值结束。

  3. 怎么理解 在编译时,一切无法确定大小或者大小可以改变的数据,都无法安全地放在栈上,最好放在堆上

    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 循环中,也可能会导致崩溃

    所以,这两种在栈上分配可变大小的数据,是不安全的

  4. 可执行文件加载string literal 到内存,如果不同的两个函数,foo() 和bar()中,各自都用到了一个string literal “hello”, 那么编译器从从可执行文件.rodata 中加载内存,是加载一份”hello”,还是两份?

    是一份。它们指向 .rodata 中同样的地址

  5. 如何确定当前开发环境里调用栈的最大大小

    在 *nix 下,你可以通过 ulimit -s 来查看和修改。linux 和 osx 默认是 8M