先问一个很基础、但很多人答不上来的问题:int a = 1 和 String s = "hello",这俩变量在内存里住的是不是同一种「房子」?
还真不是。一个住「宿舍」,一个住「宾馆」。程序员的内存从头到尾分两块地盘:栈(Stack)和堆(Heap)。平时听到的空指针、内存泄漏、double free,十个里有九个都出在这两块地盘的交界处。
今天不搬术语,用一摞盘子和一家餐厅,把这事一次讲透。
栈的规矩只有一条:后进先出。最后放上去的盘子最先被拿走,放上去叫 push,拿走叫 pop,而且只允许动最顶上那一个。
它还有个硬性要求:每个数据的大小必须在编译时就定死,后面不能变。i32 固定 4 字节、bool 1 字节、长度 10 的字节数组 10 字节——就像尺寸统一的盘子,才能摞得整整齐齐。
那栈为什么快?因为压根不用找位置。新数据永远放栈顶,所谓分配内存,就是把栈顶指针挪一下,几乎零成本。
![]()
栈:一摞盘子,push 放上去,pop 拿走
堆:一家餐厅,先找座再入席
如果数据大小编译时不知道、运行中还会变——比如用户随手输入的字符串、一个不停往里塞元素的列表——栈就伺候不了了,只能请去堆。
堆就像一家餐厅:你进门告诉服务员「我们 4 个人」,这是申请空间;服务员在店里转一圈,找一张坐得下的空桌,这是分配器在搜索空闲内存;桌子标记「已占用」,然后递给你一张号牌:「7 号桌」,这就是指针;朋友晚到了也不用急,报一句「7 号桌在哪」就能找到你们,这就是通过指针访问数据。
![]()
堆:一家餐厅,先找空桌,再领号牌
一张表,看懂这对兄弟
![]()
速度差主要体现在两个环节。一是分配:栈不用找位置,堆得满店找空桌还要登记在册。二是访问:栈直接拿,堆要多跳一步,而且数据东一块西一块。
别小看「跳来跳去」这事。数据在内存里挨得越近,CPU 缓存命中率就越高,处理就越快——跟服务员点单一个道理:A 桌 A 桌 B 桌 B 桌一路跑下来最省脚程;A 桌 B 桌 A 桌 B 桌来回横跳,时间全耗在路上了。
![]()
栈上放不下的内容,靠指针指向堆里的真实数据
调用函数时,参数和局部变量会被压进栈;函数一返回,这一整层整体弹出,自动清理,谁都不用操心。栈上数据的生命周期和函数调用严格绑定,天生不需要人管。
![]()
麻烦的是堆。函数结束了,堆上那块内存不会自己消失,于是坑全来了:
- • 没人管:7 号桌到底算谁的?搞不清就要出错
- • 重复浪费:同一拨人被安排了三张桌子,内存白白吃掉
- • 不清理:客人走了桌子一直没收,内存泄漏,最后没座位可用
- • 收了又坐:桌子收了还有人回去坐,double free / 悬垂指针,程序当场崩给你看
![]()
这也是为什么学 Rust 的人都要先被栈和堆「教育」一遍——所有权不是玄学,它就是冲着堆这块没人收拾的地盘去的。
下次再看到「空指针」「内存泄漏」这些词,脑子里可以直接切餐厅画面:要么是桌子没人收,要么是客人回了张已经被收掉的桌子。
你被内存问题坑得最惨的一次是什么?段错误查一整晚,还是泄漏把线上服务撑爆了?评论区聊聊
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.