再写一个简单的函数heap,我们连续作4次的malloc操作 。每次只申请4字节的内存,并打印对应的内存地址:
文章插图
如你所见,指针变量p的值,也就是所得内存块的内存地址,在不断升高 。这也验证了那句话:“堆”的生长方向是由低端内存,向高端内存生长的 。
或许你也发现了,虽然每次调用malloc的代码,也是非常规律的,但每次得到的内存地址,也就是指针变量p的值,都是无规律变化的!这也暗示malloc的分配策略是比较复杂的,内存碎片或许正在悄悄产生 。
06
总结
1. “栈”内存由操作系统分配给每个任务(线程)私用,不可共享 。但由于“栈”往往得不到MMU的特殊保护,所以,这种愿望或许是难以实现的 。
因为只要得到某个栈变量的地址,线程A和线程B就可以相互攻击、黑化对方的“栈” 。而“堆”内存,往往可以被多个任务(线程)共享,所以,保证数据的完整性就显得非常必要 。
2. “栈”内存的空间一般比较小,多用于存放“栈”变量、返回地址等函数的栈帧信息 。但过深的函数调用、或者递归调用,会有“爆栈”(也叫:“堆栈”溢出、stack overflow)的风险 。一般随着函数的逐层调用,函数会自动的申请“栈”内存;随着函数的逐层返回,函数也会自动的回收“栈”内存 。一般情况下,不会产生内存碎片和内存泄露 。
而堆的内存空间相对比较大,可用于存放较大的数据 。堆内存的申请、释放,只能由程序员编写相应的代码,调用特定的函数,手动申请、释放 。但随着程序的复杂,内存碎片、内存泄露会时有发生 。
3. “栈”的访问效率极高,特别是申请、释放内存的操作,都被编译器高度优化 。往往只需要一条CPU指令(push、pop),改变一下CPU寄存器rsp的值,就能完成任务 。而堆的申请、释放函数,就会复杂许多,多次使用后,还会产生内存碎片 。
至于,在没有操作系统的时候,“堆”和“栈”,就需要程序员手动划分内存空间 。相信作过单片机开发的同学,对此一定不陌生 。
07
热点问题
Q1:malloc跟“堆”(heap)是什么关系?
A1:“堆”(heap)一般是操作系统附送给应用程序的一段内存块,在程序运行的时候,如果需要动态分配一些内存的话,我们往往通过函数malloc从“堆”里面申请内存,当然,使用完毕后我们往往会通过free函数,归还刚才申请的内存,从而保证“堆”这个内存块是充裕的 。当然,如果申请的内存量过大,超过了“堆”内存的默认空间大小时,操作系统往往也提供相应的系统调用,用以扩充“堆”内存的空间 。
Q2:使用malloc做动态内存分配,会产生碎片,分配速度也慢,那使用它有什么好处呢?
A2:相对静态内存分配,动态内存分配的内存使用效率比较高 。需要使用内存的时候就申请,使用完了,就释放、归还 。这样,即使一小块内存,也可能同时满足多个线程、任务对内存的需求 。
相反,像全局变量、静态变量,它们所占据的静态内存,从编译的时候就决定了它是永久归属的 。无论当前使用与否,它们都会占据这个内存,直到程序的生命周期结束 。
Q3:如果一个函数还没有运行,程序就被用户强制退出,会产生内存泄露吗?
A3:一般情况下,是不会产生内存泄露的 。因为程序运行时,所占用的所有内存资源(包括:“堆”、“栈”、数据段、代码段),操作系统都有相应的记录 。在操作系统得到用户强制退出的命令时,操作系统往往会先释放、回收该程序占据的所有内存、文件等资源 。
Q4:“堆排序”,跟这里说的“堆”,是什么关系?
A4:没有任何关系 。“堆排序”是对一种排序算法的形象描述 。我们的这里说的“堆”是指:一般由操作系统提供给程序运行的内存块 。
【CPU眼里的:堆和栈】
推荐阅读
- 一个很强大,但用在接口参数和返回结果,会造成灾难性后果的C#语法
- 你每天用来打卡的钉钉,居然藏着「ChatGPT」「Midjourney」和「Notion」
- 每个程序员都应该了解的延迟指标
- 虚拟线程在SpringBoot中的应用
- JSX是Vue前端开发的未来吗?
- 什么是DNS域名劫持?
- 鸿蒙元服务开发实例:桌面卡片上的电动自行车助手E-Bike
- 2023 年值得考虑的10大 React 静态站点生成器!
- 苹果开源FastViT:快速卷积Transformer的混合视觉架构
- 椰汁糕的做法 香甜软绵孩子抢着吃
