您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

CPU眼里的:堆和栈

时间:2023-08-17 10:54:14  来源:今日头条  作者:阿布编程

Heap和Stack是程序运行的幕后英雄,但如果它一直是“无名英雄”的话,也会成为程序员的“隐形杀手”,让我们用CPU的眼睛,把它们看个清清楚楚

 

01

提出问题

本系列,前面的文章中,我们提及了很多次:函数“堆栈”(stack),可以说没有“堆栈”这种特殊的数据结构,就没有函数调用。

阿布也特别喜欢“堆栈”这个翻译,因为它形象的描述了“堆栈”的堆叠结构,很好的展示了该数据结构的特点。

但为了避免跟本章节讨论的另一个数据结构“堆”(heap)混淆,在本章节,我们一律将“堆栈”(stack)简称为“栈”。

其实,“堆”(heap)和“栈”(stack)并不是一个陌生的话题,相反,它们在编程实践中,经常被程序员提及。因为,不管你是否意识到它们的真实存在,你都在使用它们。因为它们如此重要,所以为了正确的区分、使用它们,很多同学都对它们的规则、特性,倒背如流。

这里我们将尝试用CPU的视角,重新认识一下“堆”和“栈”。希望能减轻一点记忆的痛苦,并给你带来一些不同的启发。

注意:我们这里说的堆和“栈”,是指程序运行所必备的“堆”和“栈”。不是由程序员自己编写的“堆”或“栈”的数据结构,虽然二者的原理相同,但使用场景并不一致。

 

 

02

“栈”的分析

好了,一切从程序的运行开始,我们编写一个世界上最简单的代码:

int mAIn()
{//thread A
}

经过编译器编译后,生成的可执行程序是a.out,随后我们双击运行。

在操作系统将a.out中唯一的函数main,加载到内存后;尽管我们没有定义任何变量、也没有进行任何函数调用,无论我们的程序需要与否,操作系统都会附送一段内存块给我们:

 

这就是“栈”。当然这个内存块不大,有几十KB的,也有几MB、几十MB的。具体大小,一般由操作系统决定。

视频“CPU眼里的:函数调用”告诉我们:这个“栈”,未来将承载着记录:函数返回地址、提供临时变量的内存空间等职责。所以,只要我们使用函数,这个“栈”就必须存在,它是函数运行的前提。

如果此时,我们又创建了一个线程B,会发生什么事情呢?

 

如你所见,我们也需要为线程B提供一个类似main函数的起始运行函数thread_main。

所以,为了保证线程B可以顺利的使用函数,操作系统也会为线程B准备一个同等大小的内存块,用来作为线程B的“栈”。从此之后,主线程A和线程B就可以独立运行了。

如果再增加一下难度,让线程A和线程B,同时调用函数func:

 

那函数func在执行完后,它怎么知道要返回到函数main,还是返回到函数thread_main呢?

其实它们的返回地址,分别存储在线程A和线程B的“栈”里面。所以,它们不会相互干扰,这样,线程A调用完函数func后,会根据自己“栈”中的信息,返回到函数main;线程B调用完函数func后,则会返回到函数thread_main。

那会不会因为线程A、线程B,对函数func的调用顺序不同,而导致函数func的返回值不同呢?

 

如你所见,尽管线程A和线程B,运行的都是同一个函数func的代码。但函数func里面的变量a,却分别保存在线程A和线程B的“栈”里面,它们是完全独立的,不会相互干扰,所以,函数func返回时,变量a的值一定是:1。当然,如果变量a是static的,那就另当别论了。

总的来说,“栈”在使用起来,往往是自动、无感、高效的。每次的函数调用、分配临时变量,都是在申请“栈”内存;每次函数返回,则是在释放“栈”内存。

所有这些操作,只需要简单移动一下栈顶指针,也就是改变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

“堆”的生长方向

再写一个简单的函数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:没有任何关系。“堆排序”是对一种排序算法的形象描述。我们的这里说的“堆”是指:一般由操作系统提供给程序运行的内存块。



Tags:堆和栈   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
CPU眼里的:堆和栈
“Heap和Stack是程序运行的幕后英雄,但如果它一直是“无名英雄”的话,也会成为程序员的“隐形杀手”,让我们用CPU的眼睛,把它们看个清清楚楚” 01提出问题本系列,前面的文章中,我...【详细内容】
2023-08-17  Search: 堆和栈  点击:(317)  评论:(0)  加入收藏
C语言中堆和栈的区别
一、前言C语言程序经过编译连接后形成编译、连接后形成的二进制映像文件由栈,堆,数据段(由三部分部分组成:只读数据段,已经初始化读写数据段,未初始化数据段即BBS)和代码段组成,如下...【详细内容】
2023-06-29  Search: 堆和栈  点击:(229)  评论:(0)  加入收藏
Java堆和栈的区别和介绍以及JVM的堆和栈
1、 Java的堆内存和栈内存Java把内存划分为两种:一种是堆内存,一种是栈内存堆:主要用于储存实例化的对象、数组。由JVM动态分配内存空间。一个jvm只有一个堆内存,线程是可以共享...【详细内容】
2020-10-23  Search: 堆和栈  点击:(194)  评论:(0)  加入收藏
C语言的内存分配方式:堆和栈
在 C 语言中,内存分配方式有以下三种形式:1、从静态存储区域分配由编译器自动分配和释放,在程序编译的时候就已经分配好内存,这块内存在程序的整个运行期间都存在,直到整个程序...【详细内容】
2019-05-05  Search: 堆和栈  点击:(2913)  评论:(0)  加入收藏
▌简易百科推荐
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(5)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(12)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(8)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(5)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(10)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(8)  评论:(0)  加入收藏
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Java技术指北  微信公众号  Tags:HashMap   点击:(11)  评论:(0)  加入收藏
如何从头开始编写LoRA代码,这有一份教程
选自 lightning.ai作者:Sebastian Raschka机器之心编译编辑:陈萍作者表示:在各种有效的 LLM 微调方法中,LoRA 仍然是他的首选。LoRA(Low-Rank Adaptation)作为一种用于微调 LLM(大...【详细内容】
2024-03-21  机器之心Pro    Tags:LoRA   点击:(12)  评论:(0)  加入收藏
这样搭建日志中心,传统的ELK就扔了吧!
最近客户有个新需求,就是想查看网站的访问情况。由于网站没有做google的统计和百度的统计,所以访问情况,只能通过日志查看,通过脚本的形式给客户导出也不太实际,给客户写个简单的...【详细内容】
2024-03-20  dbaplus社群    Tags:日志   点击:(4)  评论:(0)  加入收藏
Kubernetes 究竟有没有 LTS?
从一个有趣的问题引出很多人都在关注的 Kubernetes LTS 的问题。有趣的问题2019 年,一个名为 apiserver LoopbackClient Server cert expired after 1 year[1] 的 issue 中提...【详细内容】
2024-03-15  云原生散修  微信公众号  Tags:Kubernetes   点击:(6)  评论:(0)  加入收藏
站内最新
站内热门
站内头条