堆栈到底是个什么东西?

编程语言书籍中经常解释值类型被创建在栈上,引用类型被创建在堆上,但是并没有本质上解释这堆和栈是什么。我仅有高级语言编程经验,没有看过对此更清晰的解释。我的意思是我理解什么是栈,但是它们到底是什么,在哪儿呢(站在实际的计算机物理内存的角度上看)?
1. 在通常情况下由操作系统(OS)和语言的运行时(runtime)控制吗?
2. 它们的作用范围是什么?
3. 它们的大小由什么决定?

4. 哪个更快?

5.     函数的分配可以用堆来代替栈吗?


 在通常情况下由操作系统(OS)和语言的运行时(runtime)控制吗?
如前所述,堆和栈是一个统称,可以有很多的实现方式。计算机程序通常有一个栈叫做调用栈,用来存储当前函数调用相关的信息(比如:主调函数的地址,局部变量),因为函数调用之后需要返回给主调函数。栈通过扩展和收缩来承载信息。实际上,程序不是由运行时来控制的,它由编程语言、操作系统甚至是系统架构来决定。

堆是在任何内存中动态和随机分配的(内存的)统称;也就是无序的。内存通常由操作系统分配,通过应用程序调用 API 接口去实现分配。在管理动态分配内存上会有一些额外的开销,不过这由操作系统来处理。


它们的作用范围是什么?
调用栈是一个低层次的概念,就程序而言,它和“作用范围”没什么关系。如果你反汇编一些代码,你就会看到指针引用堆栈部分。就高级语言而言,语言有它自己的范围规则。一旦函数返回,函数中的局部变量会直接直接释放。你的编程语言就是依据这个工作的。

在堆中,也很难去定义。作用范围是由操作系统限定的,但是你的编程语言可能增加它自己的一些规则,去限定堆在应用程序中的范围。体系架构和操作系统是使用虚拟地址的,然后由处理器翻译到实际的物理地址中,还有页面错误等等。它们记录那个页面属于那个应用程序。不过你不用关心这些,因为你仅仅在你的编程语言中分配和释放内存,和一些错误检查(出现分配失败和释放失败的原因)。


它们的大小由什么决定?

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存区域。这句话的意思是栈顶的地址
和栈的最大容量是系统预先规定好的
,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时
就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较
小。  
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地
址的,自然是不连续的,而链表的遍历方向是由低地址向高地址
。堆的大小受限于计算机系统中有效的
虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。


哪个更快一些?

栈更快因为所有的空闲内存都是连续的,因此不需要对空闲内存块通过列表来维护。只是一个简单的指向当前栈顶的指针。编译器通常用一个专门的、快速的寄存器来实现。更重要的一点事是,随后的栈上操作通常集中在一个内存块的附近,这样的话有利于处理器的高速访问(译者注:局部性原理)。


函数的分配可以用堆来代替栈吗?
不可以的,函数的活动记录(即局部或者自动变量)被分配在栈上, 这样做不但存储了这些变量,而且可以用来嵌套函数的追踪。
堆的管理依赖于运行时环境,C 使用 malloc ,C++ 使用 new ,但是很多语言有垃圾回收机制。

栈是更低层次的特性与处理器架构紧密的结合到一起。当堆不够时可以扩展空间,这不难做到,因为可以有库函数可以调用。但是,扩展           栈通常来说是不可能的,因为在栈溢出的时候,执行线程就被操作系统关闭了,这已经太晚了。


答案一
栈是为执行线程留出的内存空间。当函数被调用的时候,栈顶为局部变量和一些 bookkeeping 数据预留块。当函数执行完毕,块就没有用了,可能在下次的函数调用的时候再被使用。栈通常用后进先出(LIFO)的方式预留空间;因此最近的保留块(reserved block)通常最先被释放。这么做可以使跟踪堆栈变的简单;从栈中释放块(free block)只不过是指针的偏移而已。
堆(heap)是为动态分配预留的内存空间。和栈不一样,从堆上分配和重新分配块没有固定模式;你可以在任何时候分配和释放它。这样使得跟踪哪部分堆已经被分配和被释放变的异常复杂;有许多定制的堆分配策略用来为不同的使用模式下调整堆的性能。

每一个线程都有一个栈,但是每一个应用程序通常都只有一个堆(尽管为不同类型分配内存使用多个堆的情况也是有的)。 1. 当线程创建的时候,操作系统(OS)为每一个系统级(system-level)的线程分配栈。通常情况下,操作系统通过调用语言的运行时(runtime)去为应用程序分配堆。 2. 栈附属于线程,因此当线程结束时栈被回收。堆通常通过运行时在应用程序启动时被分配,当应用程序(进程)退出时被回收。 3. 当线程被创建的时候,设置栈的大小。在应用程序启动的时候,设置堆的大小,但是可以在需要的时候扩展(分配器向操作系统申请更多的内存)。 4. 栈比堆要快,因为它存取模式使它可以轻松的分配和重新分配内存(指针/整型只是进行简单的递增或者递减运算),然而堆在分配和释放的时候有更多的复杂的 bookkeeping 参与。另外,在栈上的每个字节频繁的被复用也就意味着它可能映射到处理器缓存中,所以很快。


答案二
Stack(栈):
1. 和堆一样存储在计算机 RAM 中。
2. 在栈上创建变量的时候会扩展,并且会自动回收。
3. 相比堆而言在栈上分配要快的多。
4. 用数据结构中的栈实现。
5. 存储局部数据,返回地址,用做参数传递。
6. 当用栈过多时可导致栈溢出(无穷次(大量的)的递归调用,或者大量的内存分配)。
7. 在栈上的数据可以直接访问(不是非要使用指针访问)。
8. 如果你在编译之前精确的知道你需要分配数据的大小并且不是太大的时候,可以使用栈。
9. 当你程序启动时决定栈的容量上限
Heap(堆):
1. 和栈一样存储在计算机RAM。
2. 在堆上的变量必须要手动释放,不存在作用域的问题。数据可用 delete, delete[] 或者 free 来释放。
3. 相比在栈上分配内存要慢。
4. 通过程序按需分配。
5. 大量的分配和释放可造成内存碎片。
6. 在 C++ 中,在堆上创建数的据使用指针访问,用 new 或者 malloc 分配内存。
7. 如果申请的缓冲区过大的话,可能申请失败。
8. 在运行期间你不知道会需要多大的数据或者你需要分配大量的内存的时候,建议你使用堆。

9. 可能造成内存泄露。


答案三

堆和栈是两种内存分配的两个统称。可能有很多种不同的实现方式,但是实现要符合几个基本的概念:

        1.对栈而言,栈中的新加数据项放在其他数据的顶部,移除时你也只能移除最顶部的数据(不能越位获取)。

        2.对堆而言,数据项位置没有固定的顺序。你可以以任何顺序插入和删除,因为他们没有“顶部”数据这一概念。


 

上面上个图片很好的描述了堆和栈分配内存的方式。




答案四

你问题的答案是依赖于实现的,根据不同的编译器和处理器架构而不同。下面简单的解释一下:

1.栈和堆都是用来从底层操作系统中获取内存的。

2.在多线程环境下每一个线程都可以有他自己完全的独立的栈,但是他们共享堆。并行存取被堆控制而不是栈。


堆:
1.堆包含一个链表来维护已用和空闲的内存块。在堆上新分配(malloc或者realloc)内存是从空闲的内存块中找到一些满足要求的合适块。这个操作会更新堆中的块链表。这些元信息也存储在堆上,经常在每个块的头部一个很小区域。
2.堆的增加新块通常从低地址向高地址扩展。因此你可以认为堆随着内存分配而不断的增加大小。如果申请的内存大小很小的话,通常从底层操作系统中得到比申请大小要多的内存。
3.申请和释放许多小的块可能会产生如下状态:在已用块之间存在很多小的空闲块。进而申请大块内存失败,虽然空闲块的总和足够,但是空闲的小块是零散的,不能满足申请的大小,。这叫做“堆碎片”。

4.当旁边有空闲块的已用块被释放时,新的空闲块可能会与相邻的空闲块合并为一个大的空闲块,这样可以有效的减少“堆碎片”的产生。


 
 
栈:
1.栈经常与 sp 寄存器(译者注:”stack pointer”,了解汇编的朋友应该都知道)一起工作,最初 sp 指向栈顶(栈的高地址)。
2.CPU 用 push 指令来将数据压栈,用 pop 指令来弹栈。当用 push 压栈时,sp 值减少(向低地址扩展)。当用 pop 弹栈时,sp 值增大。存储和获取数据都是 CPU 寄存器的值。
3.当函数被调用时,CPU使用特定的指令把当前的 IP (译者注:“instruction pointer”,是一个寄存器,用来记录 CPU指令的位置)压栈。即执行代码的地址。CPU 接下来将调用函数地址赋给 IP ,进行调用。当函数返回时,旧的 IP 被弹栈,CPU 继续去函数调用之前的代码。
4.当进入函数时,sp 向下扩展,扩展到确保为函数的局部变量留足够大小的空间。如果函数中有一个 32-bit 的局部变量会在栈中留够四字节的空间。当函数返回时,sp 通过返回原来的位置来释放空间。
5.如果函数有参数的话,在函数调用之前,会将参数压栈。函数中的代码通过 sp 的当前位置来定位参数并访问它们。
6.函数嵌套调用和使用魔法一样,每一次新调用的函数都会分配函数参数,返回值地址、局部变量空间、嵌套调用的活动记录都要被压入栈中。函数返回时,按照正确方式的撤销。

7.栈要受到内存块的限制,不断的函数嵌套/为局部变量分配太多的空间,可能会导致栈溢出。当栈中的内存区域都已经被使用完之后继续向下写(低地址),会触发一个 CPU 异常。这个异常接下会通过语言的运行时转成各种类型的栈溢出异常。


 


注:文章是笔者在学习堆栈时所看到的一篇关于堆栈的帖子,从中截取了一部分比较好的答案与大家分享,这四个答案从不同角度解读堆栈,互为补充,相信对读者有所启发。

这是我学习堆栈时发现的几个好贴子连接:

原帖地址:https://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap

代码执行时内存分配图解:http://www.cnblogs.com/tianzhiliang/archive/2010/09/19/1830763.html


<think>我们正在讨论JavaScript闭包的特性。用户的问题是:为什么闭包能够访问其内部变量,即使函数执行完毕后堆栈数据通常会被清空? 根据引用[2]:函数执行会形成栈内存(执行上下文/作用域)。正常情况下,函数执行完毕,其私有栈内存会被销毁释放。但是,如果栈内存中的某个东西(一般是堆内存)被私有作用域外的事物占用了,那么不仅被占用的内容不能被释放,当前私有栈也不能被释放。闭包就是这样一种情况:内部函数引用了外部函数的变量,导致外部函数的作用域(栈内存)不能被释放,从而内部函数(闭包)可以继续访问这些变量。 具体来说,闭包的形成是由于内部函数的作用域链中包含外部函数的作用域。当外部函数执行完毕后,其作用域并不会立即销毁,因为内部函数还保持着对它的引用。这样,内部函数就可以继续访问外部函数中的变量。 引用[1]中提到:闭包中会将外部函数的自由对象添加到自己的作用域链中,所以可以通过内部函数访问外部函数的属性。 引用[3]中的例子也说明了这一点:在count函数中,内部函数fn引用了外部函数count的变量i,所以当count执行完毕,返回fn后,fn仍然可以访问和修改i。 因此,闭包能够访问其内部变量(实际上是外部函数的变量)的原因在于作用域链的维持,而外部函数的作用域(栈内存)因为被内部函数引用而没有被释放。 下面我们逐步解释: 1. 当函数执行时,会创建一个执行上下文(作用域),这个上下文包含函数内部声明的变量以及对外部作用域的引用。 2. 如果在这个函数内部定义了另一个函数(内部函数),那么内部函数的作用域链会包含外部函数的作用域。 3. 当外部函数执行完毕,正常情况下它的执行上下文应该被销毁(即栈内存释放)。但是,如果内部函数引用了外部函数的变量(即内部函数的作用域链中包含了外部函数的作用域),那么外部函数的作用域就不会被销毁,因为内部函数还在使用它。 4. 这样,即使外部函数已经执行完毕,内部函数(闭包)仍然可以通过作用域链访问到外部函数的变量。 这就是闭包能够“记住”外部变量的原因。 引用[2]中特别提到:一旦栈内存中的某个东西(堆内存)被私有作用域外的事物占用了,那么私有栈也不能被释放。这里要注意,变量i是基本类型(数字),它存储在栈内存中,但是因为被闭包引用,整个外部函数的作用域(栈内存)都不会被释放。而如果变量是引用类型(对象),那么对象存储在堆内存中,栈内存中存储的是指向堆内存的地址,闭包保留的是栈内存中的地址,从而可以继续访问堆内存中的对象。 所以,闭包机制打破了通常的函数执行完毕后栈内存释放的规则。 总结:闭包通过作用域链保留了对外部函数作用域的引用,从而使得外部函数的作用域(包括其变量对象)在函数执行完毕后不会被垃圾回收机制回收,因此闭包可以继续访问这些变量。 根据要求,我们需要在回答中引用相关段落,并生成相关问题。 引用说明: 引用[1]:说明了闭包将外部函数的自由对象添加到自己的作用域链中。 引用[2]:解释了栈内存释放的条件,以及闭包导致栈内存不被释放的原因。 引用[3]:给出了一个闭包应用的例子。 因此,在回答中,我们会引用这些内容。 同时,生成相关问题。 注意:使用中文回答,数学公式用LaTeX格式。</think>### JavaScript闭包为何能访问内部变量,即使函数执行后堆栈被清空? 闭包能持续访问内部变量的核心机制在于 **作用域链的维持** 和 **堆内存的保留**,其原理可通过以下步骤解析: --- ### 1. **函数执行时的内存分配** - 当函数执行时,会创建**执行上下文**(栈内存),其中包含: - 局部变量(如 `let i = 0`) - 对外部作用域的引用(作用域链) - 若函数内部定义了新函数(如 `fn`),该函数会**隐式保存外部函数的作用域链**(引用[1])。 --- ### 2. **闭包形成的关键:作用域链绑定** ```javascript function count() { let i = 0; // 局部变量(存储在栈内存) return function fn() { // 闭包函数 i++; return i; }; } const fun = count(); // 执行count后,栈内存本应释放 ``` - **关键点**:内部函数 `fn` 被返回并赋值给 `fun` 时,会将其外部函数 `count` 的作用域链绑定到自身(引用[1])。 - 此时,`count` 的变量 `i` 被闭包引用,**无法被垃圾回收**(引用[2])。 --- ### 3. **堆内存的保留机制** - **栈内存释放**:`count()` 执行完毕后,其栈内存(执行上下文)被销毁。 - **堆内存保留**:变量 `i` 实际存储在**堆内存**中(引用[2]),闭包通过作用域链持续持有对该堆内存的引用: ```javascript fun(); // 1(访问堆内存中的i) fun(); // 2(修改堆内存中的i) ``` - **垃圾回收规则**:只要闭包 `fun` 存在,其引用的堆内存(含 `i`)便不会被回收(引用[2])。 --- ### 4. **闭包与普通函数的区别** | **场景** | 普通函数 | 闭包 | |------------------------|-----------------------------|-------------------------------| | **函数执行后** | 栈内存释放,局部变量销毁 | 栈内存释放,但堆内存保留 | | **变量访问** | 无法再访问局部变量 | 通过作用域链持续访问外部变量 | | **内存管理** | 由垃圾回收自动处理 | 依赖闭包引用计数(手动解除需赋值 `null`)[^2] | --- ### 5. **为何不导致内存泄漏?** - **可控释放**:当闭包不再被引用时(如 `fun = null`),其绑定的堆内存会被垃圾回收(引用[2]): ```javascript fun = null; // 解除引用,堆内存被回收 ``` - **设计本质**:闭包是 JavaScript **模拟私有变量**的核心方式(引用[1]),通过作用域链保护数据,而非依赖栈内存。 --- ### 总结 闭包能访问“已销毁”的变量,是因: 1. 内部函数**绑定外部函数的作用域链**,维持对变量的引用[^1]。 2. 变量实际存储在**堆内存**中,闭包存在时堆内存不被回收[^2]。 3. 作用域链机制使闭包**跨越栈内存生命周期**,直接访问堆内存数据。 > 例如 Redux 中,闭包保留对 `state` 的引用,使 `dispatch` 能跨作用域修改状态(引用[^4])。 --- **
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值