写点什么

每日一 R「22」内存:堆与栈

作者:Samson
  • 2022 年 9 月 07 日
    上海
  • 本文字数:1716 字

    阅读完需:约 6 分钟

每日一R「22」内存:堆与栈

课程开始之前,考虑如下的代码:


let s = "hello, world".to_string();
复制代码


  • "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)、堆越界等内存安全问题。


如何解决堆内存管理问题,不同的语言选择了不同的方案。


  • Java 使用 GC 来处理堆内存的回收。GC 是一种追踪式垃圾回收方法,来自动管理堆内存(主要是回收)。

  • Object C 和 Swift 使用自动引用计数来管理堆内存。


GC 在内存分配和回收时无需额外的操作,因此效率较高。而 Arc 在分配和回收时需要处理引用计数值,因此效率较 GC 低。但是,GC 会存在 STW 问题。


我们使用 Android 手机偶尔感觉卡顿,而 iOS 手机却运行丝滑,大多是这个原因。


GC 分配和释放内存的效率和吞吐量要比 ARC 高,但因为偶尔的高延迟,导致被感知的性能比较差,所以会给人一种 GC 不如 ARC 性能好的感觉。

03-思考题

  1. 如果有一个数据结构需要在多个线程中访问,可以把它放在栈上吗?为什么?


不可以。栈是线程独占的内存空间,无法在线程之间共享。


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


可以。在栈上创建一个指针,指向栈上的变量。例如:


let s = 10;let s1 = &s;       // s1 为 s 的指针
复制代码


创建的指针仅可在当前方法中可用,不可传递到其他方法中。否则,会发生使用已释放内存。


本节课程链接《01|内存:值放堆上还是放栈上,这是一个问题

发布于: 2022 年 09 月 07 日阅读数: 54
用户头像

Samson

关注

还未添加个人签名 2019.07.22 加入

InfoQ签约作者 | 阿里云社区签约作者

评论

发布
暂无评论
每日一R「22」内存:堆与栈_学习笔记_Samson_InfoQ写作社区