程序在内存里的样子
复习
- 分支与循环:高级语言结构如何落到比较和跳转指令
- 函数:把一段工作命名、传入参数并获得结果
- 函数调用与运行栈:保存参数、局部变量和返回地址
TL;DR
- 程序在内存里大致分成几块:代码区、数据区、堆、栈
- 代码区放指令,数据区放全局数据,栈放函数调用,堆放运行时动态申请的内存
- 不同的块职责不同、权限不同,增长方向也不同
- 搞清“变量住在哪”,很多现象就说得通了
正文
我们一直在用内存:放指令、放数据、放栈帧。可它到底是怎么安排这些内容,才能各就各位、互不打架?这一章,把程序在内存里的全貌铺开来看。
一张内存地图
一个正在运行的程序,内存大致长这样:
高地址 ┌───────────────┐
│ 栈 │ ← 向下增长
│ ↓ │
│ │
│ ↑ │
│ 堆 │ ← 向上增长
├───────────────┤
│ 数据区 │ 全局变量、常量
├───────────────┤
│ 代码区 │ 指令(通常只读)
低地址 └───────────────┘
一块一块看。
代码区
代码区放的是 指令 ,也就是编译好后的一串机器码。从“第一次呼吸”起,PC 就是在这片区域里指来指去的。
它通常被设为 只读 :程序运行时不应该去改自己的指令。这也是一道安全防线——万一有人想篡改代码,硬件直接拦下。
数据区
数据区放 全局数据 :在函数外定义的变量、写死的常量等。它们在程序一开始运行时就分配好,直到程序结束才释放。
因为地址在加载时就定了,全局变量的位置比较“稳定”,但也因此一直占着内存。
栈:自动来去
栈,上一章刚讲过,专门伺候函数调用。参数、返回地址、局部变量都放在这里。
它有两个特点:
- 自动 :函数一调用就分配,一返回就回收,不用程序员操心
- 向下增长 :新栈帧压在更低的地址上
局部变量“出了函数就没了”,正是因为它的家——栈帧——在函数返回时被收走了。
堆:按需申请
栈的空间有限,而且讲究“随调用来、随返回走”。可有些数据活得更久、更大,没法塞进栈里。比如“读入一个不确定长度的列表”,事先根本不知道该留多少地方。
这时就用 堆 (heap)。堆是一大片空闲区域,程序 在运行时按需申请 :需要一块内存,就用相应的方式向系统要;用完了,再还回去。它不随函数返回自动消失,什么时候释放,得由程序自己(或它的运行时)决定。
堆向上增长,栈向下增长,两者迎面靠近——所以只要栈和堆撞上,就说明内存不够用了。
变量到底住在哪
把前面这些串起来,就能回答一个常见问题:变量住在哪?
| 变量种类 | 住在哪 | 何时分配 | 何时释放 |
|---|---|---|---|
| 全局变量 | 数据区 | 程序加载时 | 程序结束时 |
| 局部变量 | 栈 | 函数调用时 | 函数返回时 |
| 动态申请的内存 | 堆 | 运行时按需 | 程序显式释放 |
这张表能解释很多现象:为什么局部变量出了函数就访问不到了,为什么动态申请的内存忘记释放会越用越多,为什么递归太深会栈溢出。
至此,一个“程序”的骨架就齐了:有指令、有数据、有栈、有堆。但我们还欠着一笔账——这个程序,究竟是怎么从我们写的那几行文字,变成内存里这副样子的?
思考题
局部变量和动态申请的内存,一个放栈、一个放堆。它们各自适合保存什么样的数据?为什么“一个不知道长度的列表”更适合放堆,而不是栈?
小结
知识点
- 程序在内存里分为代码区、数据区、堆、栈
- 代码区放指令,通常只读
- 数据区放全局数据
- 栈保存函数调用,自动分配与回收,向下增长
- 堆按需申请,向上增长,手动管理
参考资料
- Wikipedia(zh):内存布局:程序的内存分区
- Wikipedia(zh):栈 (数据结构):调用栈
- Wikipedia(zh):动态内存分配:堆与内存管理
思考题答案(仅供参考)
栈适合“随函数调用而生、随函数返回而死”的小块数据,比如参数和局部变量;堆适合大小不定、生命周期长、需要跨函数存活的数据。一个不知道长度的列表,事先不知道要留多大地方,而且往往要在多个函数之间传递、活得更久,这些都更适合放到堆上按需分配。栈讲究来去分明,放这种数据就不合适了。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪