所有这些操作,只需要简单移动一下栈顶指针,也就是改变CPU寄存器rsp的值,就可以完成了:
文章插图
甚至,对“栈”内存的读、写操作,往往也是一条CPU指令,就能解决 。
有趣的是:函数也不知道会有多少个线程调用它 。所以,哪个线程调用它,它就操作哪个线程的“栈”:
文章插图
当然,这些规则比较隐晦,需要我们对函数运行原理,有比较清晰的认识 。如果大家不清楚阿布在说什么,请回看一下上个章节“CPU眼里的:{函数括号}” 。
03
“栈”的生长方向
好了,说了这么多假、大、空的话,该做点实事了 。写一个简单的函数stack,打印一下函数内临时变量a的值和地址,随后继续递归调用函数stack:
文章插图
如你所见,随着函数的调用,变量a的值没有变化,一直是0;但它的地址一直在变化 。从趋势上看,变量a的内存地址的值,在逐层降低 。这也验证了那句话:“栈”的生长方向、或者说消耗、申请方向,是由高端内存,向低端内存“生长”的 。
同时,由于我们的递归调用十分的规律,所以,每个变量a之间的内存地址间隔也是非常固定的32(0x20)个字节 。
04
“堆”的分析
好了,说完“堆”,我们再说“堆” 。跟“栈”一样,一般情况下,“堆”也是操作系统附送给我们的 。不同的是,“堆”的内存空间往往比较大,可以用来存放一些超大的数据 。
而且不同于“栈”的隐晦,“堆”的使用,非常明确、清晰:
int main(){int* p = (int*)malloc(4);free(p);p = (int*)calloc(4, 1);free(p);realloc(&p, 4);free(p);p = new int(10);delete p;}程序员需要通过malloc、calloc、realloc函数或new操作来申请堆内存 。只要能得到这个内存块的地址,线程A、线程B都可以随时访问这块内存 。至于释放“堆”内存,也需要程序员通过手动的调用free或delete来归还、释放内存 。
总的来说,“堆”在使用起来,非常直接,看上去也比较可控 。但malloc之后,忘记free的事情,也时有发生 。这也是大家常说的:内存泄露 。但阿布感觉这更像是:借钱不还 。
另外,相比“栈”的高效操作,“堆”的申请、释放,就显得比较慢 。因为,malloc、free本身就是一个比较复杂的函数 。需要对每次的内存申请操作,进行管理 。
这是linux的祖先minix关于malloc函数的代码链接:
https://Github.com/Stichting-MINIX-Research-Foundation/minix/blob/master/lib/libc/stdlib/malloc.c
它的总代码量达到1300多行 。而且,在多次malloc和free后,一大块完整的堆内存,会慢慢变得支离破碎,这也就是大家常说的:内存碎片 。
例如,这是一段完整的“堆”内存块:
文章插图
先做3次malloc操作:
文章插图
再做1次free操作:
文章插图
如你所见,我们现在已经无法从“堆”中,分配出一个连续的3KB的内存块了 。
05
“堆”的生长方向
推荐阅读
- 一个很强大,但用在接口参数和返回结果,会造成灾难性后果的C#语法
- 你每天用来打卡的钉钉,居然藏着「ChatGPT」「Midjourney」和「Notion」
- 每个程序员都应该了解的延迟指标
- 虚拟线程在SpringBoot中的应用
- JSX是Vue前端开发的未来吗?
- 什么是DNS域名劫持?
- 鸿蒙元服务开发实例:桌面卡片上的电动自行车助手E-Bike
- 2023 年值得考虑的10大 React 静态站点生成器!
- 苹果开源FastViT:快速卷积Transformer的混合视觉架构
- 椰汁糕的做法 香甜软绵孩子抢着吃
