栈和堆的区别

178 阅读10分钟

栈和堆在不同的场景下所代表的含义不同,一般情况有两层含义:

  • 程序内存布局场景下,栈与堆表示两种内存管理方式。
  • 数据结构场景下,栈与堆表示两种常用的数据结构。

程序内存场景中

  • 由操作系统(Operating System, OS)自动分配和释放,用于存放函数的参数、局部变量等。其操作方式和数据结构中的栈类似。

  • 例如其函数定义的局部变量按照先后定义的顺序依次压入栈中,也就是相邻变量的地址之间不存在其他变量,栈的内存地址生长方向由高到低,后定义的变量的地址低于先定义的变量。

  • 栈中存储数据的生命周期随着函数的执行完成而结束。

  • 一般由开发人员分配和释放,若开发人员不释放,程序结束后由 OS 回收。其分配方式和链表类似。

  • 堆内存地址生长方向与栈相反,由低到高。需要注意的是:后申请的内存空间并不一定在先申请的内存空间的后面,原因是先申请的内存空间一旦被释放,后申请的内存空间会利用先前释放的内存,从而导致先后分配的内存空间在地址上不存在先后关系。

  • 堆中存储的数据若未被释放,其生命周期等同于程序的生命周期。

  • 堆的内存空间分配过程:

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

区别:

栈和堆是两种基本的内存的管理模式,在设计目的、内存分配、应用场景有明显的区别。理解这些区别对于优化程序性能避免内存泄漏至关重要。

  1. 分配方式

  • 栈:有静态分配和动态分配两种分配方式。静态分配由操作系统自动分配和释放,比如局部变量的分配。动态分配由 alloca 函数进行分配。这两种分配方式都无需开发人员手动控制。
  • 堆:只有动态分配,由开发人员控制分配和释放工作,容易产生内存泄漏。
  1. 存放内容

  • 栈:存放函数返回地址、相关参数、局部变量和寄存器内容等。
  • 堆:存放动态分配的数据,如大型对象和数组。
  1. 内存大小

  • 栈:大小有限,通常由操作系统限制。--- 每个进程拥有的【栈的大小 要远远小于 堆的大小】。进程栈的大小 64bit 的 window 默认 1MB,64bits 的 Linux 默认 10MB。
  • 堆:大小灵活,理论上只受限于系统内存。
  1. 缓存方式

  • 栈:使用的是一级缓存, 通常都是被调用时处于存储空间中,调用完毕立即释放。
  • 堆:是存放在二级缓存中,调用这些对象的速度要相对来得低一些。
  1. 分配效率(性能)

  • 栈:访问速度快,一是操作系统自动内存分配和释放,会在硬件层级对栈提供支持;二是分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
  • 堆:访问速度较慢,需手动管理内存。由库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。
  1. 生长方向

  • 栈:栈的生长方向向下,内存地址由高到低。
  • 堆:堆的生长方向向上,内存地址由低到高。
  1. 应用场景

  • 栈:存放生命周期短、大小固定的数据,如函数调用栈。
  • 堆:存放生命周期长、大小不固定的数据,如动态数组、链表等。
  1. 常见问题

    无论是堆还是栈,在内容使用的时候都要防止非法越界,越界导致的非法内存访问可能会摧毁程序的堆,栈数据,轻则导致程序运行处于不确定状态,获取不到预期结果,重则导致程序异常崩溃,这些都是我们编程时与内存打交道时应该注意的问题。
    • 栈溢出:当栈空间不足时,会导致程序崩溃。
    • 内存泄漏:堆内存未及时释放,导致内存浪费。
    • 性能优化:合理使用栈和堆,可以提升程序性能。

选择使用栈或堆需考虑:

使用栈的情况:

  1. 局部变量和函数参数:如果变量的作用域仅限于函数内部,并且不需要在函数之间共享数据,可以使用栈。栈上的局部变量和函数参数在函数调用结束时会自动销毁,无需手动释放内存。
  2. 小内存需求:如果需要分配较小的内存块,并且栈的大小足以满足需求,可以使用栈。栈内存的分配和释放速度较快,不需要动态内存管理的开销。
  3. 对象的生存周期与作用域一致:如果变量的生存周期与其所在的作用域一致,并且不需要在作用域之外继续使用变量,可以使用栈。

使用堆的情况:

  1. 动态内存分配:如果需要在运行时动态地分配内存,并且在程序的不同部分之间共享数据,可以使用堆。例如,当需要在程序的不同函数之间传递数据时,可以在堆上分配内存,并将指针传递给函数。
  2. 大内存需求:如果需要分配大块内存,而栈的大小有限制,无法满足需求,可以使用堆。堆内存的大小没有固定限制,可以根据需要动态分配大块内存。
  3. 对象的生存周期:如果需要在变量的生存周期超出其作用域的情况下继续使用变量,可以使用堆。堆上分配的对象可以在堆上存活很长时间,直到手动释放。

数据结构场景中

数据结构中,堆与栈是两个常见的数据结构,理解二者的定义、用法与区别,能利用堆与栈解决很多实际问题。

栈:

  • 栈是一种运算受限的线性表,其限制是指只允许在表的一端进行插入和删除操作,这一段被成为栈顶,相对的,把另一端成为栈底。把新的元素放入栈顶元素的上面,使之成为新的栈顶元素称作进栈、入栈或压栈;把栈顶元素删除,使其相邻的元素成为新的栈顶元素称作为出栈或退栈。这种受限的运算使栈拥有先进后出的特性。

  • 栈通常保存着我们代码执行的步骤,如一个值类型的变量的初始化或者一个方法的声明。可以把栈想象成一个接着一个叠放在一起的盒子。当我们使用的时候,每次从最顶部取走一个盒子。同样,我们的栈也是如此,当一个方法(或类型)被调用完成的时候,就从栈顶取走,接着下一个,这也就是我们常说的 “先进后出” 。

  • 栈分顺序栈和链式栈两种。栈是一种线性结构,所以可以使用数组或链表(单向链表、双向链表或循环链表)作为底层数据结构。使用数组实现的栈叫做顺序栈,使用链表实现的栈叫做链式栈,二者的区别是顺序栈中的元素地址连续,链式栈中的元素地址不连续。

  • 栈的基本操作包括初始化,判断栈是否为空、入栈、出栈以及获取栈顶元素等。

堆:

  • 堆是一种常用的树形结构,是一种特殊的完全二叉树,当且仅当满足所有节点的值总是不大于或不小于其父节点的值的完全二叉树被称之为堆,堆的这一特性称之为堆序性。因此,在一个堆中,根节点是最大(或最小)节点。如果根节点最小,称之为小顶堆(或小根堆)。如果根节点最大,称之为大顶堆(或大根堆)。

  • 堆的左右孩子没有大小的顺序。堆的存储一般都是用数组来存储堆。

  • 堆存放的多是对象、数组等。堆像是一个仓库,储存着我们使用的各种对象等信息,当我们需要调用的时候,会去里面自行寻找并调用。跟栈不同的是它们被调用完毕不会立即被清理掉。

堆栈数据结构区别:

堆:堆可以被看成是一棵树,如:堆排序。

栈:一种先进后出的数据结构。

总结

  1. 内存堆栈与数据堆栈

    • 内存堆栈:存在内存中的两个存储区(堆区,栈区)。

    • 栈区:存放函数的参数、局部变量、返回地址据等值,由编译器自动释放。

    • 堆区:存放着引用类型的对象,由CLR释放。

    • 数据堆栈:是一种后进先出的数据结构,它是一个概念,主要是栈区。

  2. 栈的深入讲解

    栈(Stack)最明显的特征就是“先进后出”,本质上讲堆栈也是一种线性结构,符合线性结构的基本特点:即每个节点有且只有一个前驱节点和一个后续节点。栈把所有操作限制在"只能在线性结构的某一端"进行,而不能在中间插入或删除元素。我们把数据放入栈顶称为入栈(push), 从栈顶删除数据称为出栈(pop)。

    分配方式:当主函数调用另外一个函数的时候,要对当前函数执行断点进行保存,需要使用栈来实现: 1.首先入栈的主函数下一条语句的地址,即扩展指针寄存器的内容,然后是当前栈帧的底部地址,即扩展机制指针寄存器内容。

    2.然后是被调用函数的实参等,一般情况下是按照从右到左的顺序入栈,之后是被调函数的局部变量,注意静态变量是存放在数据段或者 BSS 段,是不入栈的。出栈的顺序正好相反。

    3.最终栈顶指向主函数下一条语句的地址,主程序又从该地址开始执行。

  3. 堆的深入讲解

    堆(Heap)是一块内存区域,与栈不同,堆里的内存能够以任意顺序存入和移除。

    分配方式: 一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。

  4. GC(Garbage Collection)垃圾回收

    前端GC(垃圾回收)是一种自动内存管理机制,主要用于 JavaScript 中,负责监测不再使用的对象并释放它们占用的内存空间。这有助于防止内存泄漏和资源浪费,确保程序的平滑运行。前端 GC 的工作原理是通过追踪记录每个对象的引用次数,当引用数达到 0 时,表示该对象不再被使用,此时可以回收其占用的内存空间。

相关优秀文章