每日一 R「22」内存:堆与栈
课程开始之前,考虑如下的代码:
"hello, world"
是一个字面常量(string literal),在编译阶段被存储到可执行文件的 .RODATA 段(GCC)或者 .RDATA 段(VC++);程序加载时,获得一个固定的内存地址。to_string
方法会在堆上分配一块空间,并把 hello, world 逐字节拷贝过去。赋值语句(或者说变量绑定)将堆上数据赋值给 s 时,s 作为栈上的变量,它需要知道堆上数据的内存地址、堆上已分配空间的容量、数据实际占用的长度。所以,s 拥有三个内存字(word),分别用来存储这三个信息。64 位系统上,一个 word 为 8 字节;32 位系统商,一个 word 为 4 字节。
堆和栈上分别存储什么样的数据?或者说开发时,如何确定哪些数据或内容存放在堆上,哪些存放在栈上?
01-栈
栈是程序运行的基础。在计算机内存中,栈是自顶向下(高地址向低地址)扩张的,堆是自底向上扩张的。
当函数被调用时,会在栈顶分配一块连续的空间,称为栈帧(frame)。栈帧中保存有函数调用时寄存器等上下文的值,并在函数调用结束时用这些值来恢复寄存器等上下文的内容。
如何确定栈帧大小,或者说在函数调用时,需要在栈中分配多少连续的空间?这在编译时可以确定。编译时,函数是最小的编译单元。编译器知道,在函数执行时需要哪些寄存器、会定义哪些局部变量。寄存器的大小是固定的,局部变量大小也必须是编译时可知的,以便编译器可以预留空间。
由此可以得出以下结论:
在编译时,一切无法确定大小或者大小可以改变的数据,都无法安全地放在栈上,最好放在堆上.
根据上面的结论,我们可以知道哪些数据存放在堆上,哪些数据存放在栈上。在文章开头的例子中,我们也能明白为什么 Rust 中的 String 类型的变量会使用三个内存字的胖指针指向堆上的一块空间。
数据存放在栈上的优点与不足:
栈的内存分配是非常高效的。分配与回收仅需要改动指针即可,不涉及额外计算、不涉及系统调用,仅修改寄存器。
局限性(感觉不能称之为缺点)是过大的栈内存分配会导致栈溢出,特别注意递归调用问题。
另外一个局限性就是,栈帧的大小是固定的,不能处理动态增长的内存。
02-堆
当需要动态大小的内存时,只能使用堆,例如可变长度的数组、列表、哈希表、字典等。
在堆上分配内存时往往分配的空间(容量)会大于实际需要,换句话说会预先多分配一部分内存,主要是因为堆内存分配会调用系统调用,代价昂贵,要避免频繁调用。
除了动态大小的内存需要分配在堆上,动态声明周期的内存也需要分配在堆上。存储在栈中的局部变量会随着栈帧的回收而被释放,所以栈上变量的声明周期是不受开发者控制的,且局限在当前调用方法内部。堆上分配的内存需要显式地释放,使得其具有更灵活性的生命周期,也可以在多个栈帧(或者说多个方法)之间共享。
数据存放在堆上的优点和不足:
堆内存的申请和释放比较灵活,可由开发人员自由掌控。
如果堆内存的释放完全依赖使用者,则极容易出现内存泄漏;甚至不恰当地使用或释放堆内存,还会发生使用已释放内存(use after free)、堆越界等内存安全问题。
如何解决堆内存管理问题,不同的语言选择了不同的方案。
GC 在内存分配和回收时无需额外的操作,因此效率较高。而 Arc 在分配和回收时需要处理引用计数值,因此效率较 GC 低。但是,GC 会存在 STW 问题。
我们使用 Android 手机偶尔感觉卡顿,而 iOS 手机却运行丝滑,大多是这个原因。
GC 分配和释放内存的效率和吞吐量要比 ARC 高,但因为偶尔的高延迟,导致被感知的性能比较差,所以会给人一种 GC 不如 ARC 性能好的感觉。
03-思考题
如果有一个数据结构需要在多个线程中访问,可以把它放在栈上吗?为什么?
不可以。栈是线程独占的内存空间,无法在线程之间共享。
可以使用指针引用栈上的某个变量吗?如果可以,在什么情况下可以这么做?
可以。在栈上创建一个指针,指向栈上的变量。例如:
创建的指针仅可在当前方法中可用,不可传递到其他方法中。否则,会发生使用已释放内存。
本节课程链接《01|内存:值放堆上还是放栈上,这是一个问题》
版权声明: 本文为 InfoQ 作者【Samson】的原创文章。
原文链接:【http://xie.infoq.cn/article/066ec06cb186993421d7bfad2】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论