16堆和栈的区别

很多时候我们会把堆和栈合起来叫作堆栈。在数据结构里,一般堆栈就是指的后进先出的栈(stack),但在程序内存中,堆和栈都是内存中的一种,都可以用来存放计算数据,但是堆和栈又是有区别的。

堆和栈在程序的布局中是两个不同的组成部分。事实上,堆和栈都是内存的一个组成部分,通常把堆和栈联合起来称为堆栈。实际上堆是堆,栈是栈,堆和栈是完全不同的两个概念。下面从多个方面来区分堆(Heap)和栈(Stack)。

1)内存分配

堆:由程序员负责申请,并提供申请的大小。在C语言中堆内存的分配是用malloc()函数或realloc()来完成的;在C++中用new运算符来分配。堆在使用完成后,也须由程序员显式的释放。在C语言中释放的函数为free(),在C++中是用delete操作符负责释放。若程序员忘记释放,就可能发生内存泄漏。程序结束时可能由OS回收。

栈:由系统自动分配与释放。例如,声明在函数中一个局部变量 int b;系统自动在栈中为b开辟空间。栈用语存放函数的参数值,局部变量的值等。

在内存资源分配的具体实现上,堆和栈也是有区别的:

堆:堆的分配是通过操作系统的一个记录空闲内存地址的链表来实现的。当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的delete语句才能正确的释放本内存空间。另外由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

栈:每个进程都拥有自己的一个固定连续的栈空间。在系统分配的时候,只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

2)大小限制

堆:是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在Windows下,栈的大小是固定的(是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示栈溢出。因此,能从栈获得的空间较小。

Windows:

应用栈:默认1M,编译指令/stack可改设其他值

内核栈:系统根据CPU架构而定,x86系统上为12KB,x64系统上为24KB,安腾系统上为32KB

Linux:

内核栈:4K或8K

在thread_info.h:

#ifdef CONFIG_4KSTACKS
#define THREAD_SIZE          (4096)
#else
#define THREAD_SIZE          (8192)

应用层:10M

3)效率比较:

堆:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。值得注意的是,在Windows下,最好的方式是用 VirtualAlloc()分配内存,它既不是在堆也不是在栈,而是直接在进程的地址空间中保留一块内存,该方式虽然用起来最不方便,但是速度却更快,也最灵活。

栈:由系统自动分配,速度较快。但程序员是无法控制的。

4)存放内容

堆:一般是在堆的头部用一个字节存放堆的大小,剩余部分存储的内容由由程序员根据程序计算的需要决定。

栈:栈是用来记录程序执行时函数调用过程中的活动记录(栈帧)。在函数调用时首先入栈的是参数,在大多数的C编译器中,参数是由右往左入栈,然后是返回地址,紧接着是老ebp寄存器中的值,然后是分配函数中的局部变量。需要特别指出的是,静态变量存储在静态存储区,所以不入栈。当本次函数调用结束后,按照栈后进先出的顺序退栈,即局部变量先出栈,然后是老ebp和返回地址,然后是各个参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

本页共33段,1941个字符,4782 Byte(字节)



内存为啥要分堆栈在编程里,要是全部只用堆或者全部只用栈,行不行?

硬件上这两者并没有什么特别的不同,主要是逻辑上的区别,由操作系统负责管理,因为为了提高内存的使用效率,要及时释放不需要的内存,有的内存需要一直使用,有的内存只是临时占用,前者就是堆 ,后者就是栈。理论上全用堆是可行的,但是这就要求程序员自己管理内存,不要操作系统介入,记得及时释放,否则很快就会消耗完。

栈:就像我第一句话说的,本没有什么堆栈之分,但是编程语言的出现,就有了一个概念“函数”,这个函数之间是可以相互调用的(彼此的局部变量可以通过不同的传参类型,被调函数可以独立于调用函数,也可以影响调用函数),也可以嵌套调用,调用时,需要回归到原来的代码执行顺序,需要一个先进后出的机制(就像经过若干十字路口到了一个地方后,归程时其记忆的路口需要按先进后出的顺序取出)。那么这时栈就出现了,而且它还有一个特点那就是线程独有(所以可以存放我们的临时变量),生命周期是随线程的。当然我所说的是内存栈的意思,其实“栈”就是个数据结构,是一种限定仅在表尾进行插入和删除操作的线性表,这个特性不正好是符合我刚才说的先进后出嘛。所以你可以这么理解c++或者java(jvm)中的内存栈的概念,就是编程语言的作者为了管理内存使用了“栈”这种数据结构(说的再细点就是现代CPU体系结构决定了栈是管理函数调用和局部变量的最佳数据结构。因为CPU已经提供了现成的指令)。

堆:可算是一种特殊的数据结构,好像我们经常使用的二叉树。内存堆这个解释起来就更简单了,就是一块能自由分配的内存。它允许程序在运行时动态地申请某个大小的内存空间,比如:程序员向操作系统申请一块内存,当系统收到程序的申请时,会遍历一个记录空闲内存地址的链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。其特点就是分配的速度较慢,地址不连续,容易碎片化并且是由程序员申请,同时也必须由程序员负责销毁,否则导致内存泄露。像在java这种高级语言中,我们不比担心内存回收的问题,那是因为jvm已经在帮我们处理了。

一、栈

栈具有先进后出,后进先出特性,连续存储,操作简单,使用方便,无需管理,大部分芯片都对栈提供芯片级别的硬件支持,只需要移动指针就可以快速实现内存的分配和回收。比如局部变量使用栈内存,减少不必要的内存分配管理。

栈创建和删除的时间复杂度是O(1),速度快。但是不利于管理大内存,栈中的数据大小和生存周期都是确定的,缺乏灵活性。

二、堆

堆内存的管理机制相对复杂,有一套相应的分配策略,防止大量小碎片出现,同时加快查找。堆用于动态创建分配内存,创建和删除节点的时间复杂度是O(logn)

堆的回收机制也复杂很多,根据内存大小不同,数据生命周期不同,采用相应的回收机制,涉及操作系统的堆管理。

因为堆内存的管理和申请相对复杂,更消耗系统资源,通常生命周期更长使用范围更广的全局变量使用堆内存。

堆和栈是不能相互替代的。程序的结构是前部是栈,中部是程序体,后部是堆。前部和中部是链接的时候由编译器算好了的,执行过程中是固定不变的。而后部则可以随着程序的执行而改变长度。

那么为什么要把内存分成堆和栈呢?

栈是用来存储程序初期设定的变量的,这些变量在程序执行之前就要准备好。因此栈要放在程序的前部并且固定位置,否则程序就不知道该到那里去找这些变量。而程序的运行结果往往是不能预先确定的,所以把堆放在后部以便可以提供足够的内存保存运算结果。

至于栈用先进后出的方式,读写速度高,这是针对它的长度固定的特征设计出来的。而堆为了保持可扩张的特性,也设计了类似于二叉树的索引方式,以便可以高速地寻址,并可以方便地释放内存空间。

堆,是自由分配的内存,几乎在程序运行的任意时间都可以申请获得任意的大小(假设空闲内存充足),使用完之后在任意时间都可以释放。堆灵活的使用规则可以提高内存的使用效率,就是在需要时按需分配,不需要时释放以作他用。

栈,是遵守后进先出顺序的内存,只有运行到所在的作用域才会分配,在作用域内屏蔽掉之前同名的内存的访问,在退出作用域时释放掉以让之前同名的内存能被访问。栈的后进先出顺序有效地解决同名内存的问题,并有助于编程者掌控逻辑结构(例如函数调用等)。

如果把内存比作火箭的推进器,堆就是助推器,栈是主体的各级推进器。助推器可根据实际需要不安装、少安装或多安装,并且可随时分离。主体各级推进器,要先安装最高一级再安装下一级,最后安装一级推进器,使用的时候和安装是相反的顺序,只有下一级的推进器分离了才能使用上一级的。