栈,以及栈溢出,你真的了解什么是栈吗?帮助你理解程序运行背后的故事!

907 阅读8分钟

提前声明:本人是一名自学前端开发工作者JSer,对于CS相关基础知识全是自学,如果大家发现我分享的知识有不对的或者说又想补充或者讨论的直接评论区留言,欢迎大家查阅指正!

堆和栈的概念存在于两种知识体系中,一种是数据结构,一种是操作系统。数据结构的概念相对简单,那么我们我们来解释一下操作系统中的堆和栈?

我们先来了解一下操作系统中的内存,什么是内存?

物理内存和虚拟内存

我们都知道计算机存储的物理介质有硬盘和内存,这两者都是物理的,是计算机中的重要部件,拆开电脑能真真切切摸到的东西。计算机可以连接物理存储进行读写。

在早些年的操作系统中,程序直接访问和操作的都是物理内存。这种形式无法保证系统的安全和稳定,破坏性高;而且很难同时运行多个程序,类比一下JS的单线程,可想而知,同时使用一套存储,即使是最简单的同时运行多个程序这种情况也会变得难以实现。

虚拟内存是现代操作系统普遍使用的一种技术,基本思想是:通过对主存进行抽象,为每个进程提供独立的逻辑空间。

程序运行时会创建进程,进程有自己的内存,往往称为进程地址空间进程的内存空间只是虚拟内存,而程序运行时需要实实在在的内存,即物理内存(RAM)。在必要的时候,操作系统会将程序运行中申请的内存(虚拟内存)映射到RAM,让进程能够使用物理内存。

程序被加载到内存中,这个过程往往不是一次性全量加载,而是一段一段的。

程序运行时会创建进程,进程有自己的虚拟内存空间,而虚拟内存空间一般分为两部分,应用程序使用的内存空间(process virtual memory)和系统内核程序(kernel virtual memory)使用的内存空间。同时程序运行时可能会产生很多线程。

无论是进程还是线程,本质都是CPU的工作时间段的描述。CPU要运行,环境必须先准备好,内存要ready。所以进程和线程都需要内存空间。

堆(heap)和栈(stack)是两种内存的管理形式。它们的主要区别是stack按次序排放,大小明确;heap结构则不固定,是一种可动态分配和释放的内存。单从这一点看,stack的寻址速度要比heap快,heap的灵活性则比较高。一般来说,每个线程分配一个stack,每个进程分配一个heap

程序运行中的内存也因此就有了堆内存和栈内存。

基本存放规则是:局部的,占用空间确定的数据,一般都放在stack中,反之就放在heap中。stack比如:js中的函数、局部变量、闭包。 heap:全局的类、对象

Javascript堆栈

JS的堆栈?JS有堆栈?其实单纯写JS本身不需要关心这个问题。当然弄清楚更好。

因为JS脚本语言的特性,通常运行在浏览器中,所以执行引擎比较多,多数使用C/C++实现,也有使用java实现的。最流行的Chrome浏览器的V8就是使用C++实现的。

但是可以这样理解:

  • 如果是基础类型,那栈中存的是数据本身。
  • 如果是对象类型,那栈中存的是堆中对象的引用。

所以某种意义上Javascript和Java就有点类似了,它们都是运行在一个程序上面,且这个程序都是C/C++写的。V8使用类似于JVM和大多数其他语言的堆。

接下来聊聊栈溢出

在《C语言程序的内存布局(内存模型)》中我们讲到,程序的虚拟地址空间分为多个区域,栈(Stack)是其中地址较高的一个区域。栈(Stack)可以存放函数参数、局部变量、局部数组等作用范围在函数内部的数据,它的用途就是完成函数的调用。

栈内存由系统自动分配和释放:发生函数调用时就为函数运行时用到的数据分配内存,函数调用结束后就将之前分配的内存全部销毁。所以局部变量、参数只在当前函数中有效,不能传递到函数外部。

在计算机中,栈可以理解为一个特殊的容器,大家应该都了解栈采用的是先进后出(First In Last Out)原则。放入数据常称为入栈或压栈(Push),取出数据常称为出栈或弹出(Pop)

22126181-8ae96c03350fbbd5.jpeg

栈的大小以及栈溢出

对每个程序来说,栈能使用的内存是有限的(每个线程分配的栈的内存大小是有限的),一般是 1M~8M,这在编译时就已经决定了,程序运行期间不能再改变。如果程序使用的栈内存超出最大值,就会发生栈溢出(Stack Overflow)错误。

栈内存的大小和编译器有关,编译器会为栈内存指定一个最大值,在 VC/VS 下,默认是 1M,在 C-Free 下,默认是 2M,在 Linux GCC 下,默认是 8M。

当然,我们也可以通过参数来修改栈内存的大小,这里我们不做详解,有兴趣的同学可以下去自己了解一下。

通过垃圾回收机制(GC)来帮助我们理解

我们知道,JavaScript中的变量主要分为两种类型:基本类型和引用类型

基本类型的值存储在栈(stack)内存中,而引用类型值的存储需要用到栈内存和堆(heap)内存,栈内存保存着变量的堆内存地址,地址指向的堆内存空间保存着具体的值。 栈中变量的值在使用完后会被立即回收,而堆中变量的值不会立即回收,需要手动回收或使用某种策略进行回收。

JavaScript具有自动垃圾回收机制,不需要像C/C++语言那样需要开发者手动跟踪内存的使用情况。原理很简单:找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性地执行这一操作。

那么,怎么判断哪些变量有用哪些变量没用呢?

以函数来说,函数中的局部变量只在函数执行过程中存在,在这个过程中,会为局部变量在栈或堆内存上分配相应的空间,以便存储它们的值。然后在函数中使用这些变量,直到函数执行结束。这时,局部变量就没有存在的必要了,因此可以释放它们的内存以供将来使用。这种情况下很容易判断变量是否有还有存在的必要;但并非所有情况下(如闭包)都这么容易就能得出结论。垃圾收集器必须跟踪有用或无用的变量,对不再有用的变量打上标记,以备将来收回其占用的内存。用于标识无用变量的策略可能会因实现而不同,但具体到浏览器中但实现,通常有两个策略:“标记清除”和“引用计数”

注:以上内容摘自《JavaScript高级程序设计(第3版)》

由于操作系统对每组线程的栈内存有一定的限制,为适应线程各种操作系统,所以 Node.js 默认的栈大小为 984k。不过,由于不同版本的 Node 集成的 V8 版本和优化等不同,即使同样 size 的栈空间,调用栈的栈深浅各不相同。

简单点的总结就是: 栈是一个存储数据的容器,容器有大小,内存有最大限制,所以当数据存储超过最大内存限制后,就出现栈溢出。

希望大家通过以上的分享能更好的理解程序运行背后的故事,希望能够让一些前端工作者,对闭包,闭包的优缺点以及内存泄漏的理解有所深入思考和帮助。

参考资料:

栈(Stack)是什么?栈溢出又是怎么回事?

www.codenong.com/js7cdf8ab2d…

深入理解 V8 的 Call Stack

zhuanlan.zhihu.com/p/46993552

操作系统的堆和栈

developer.aliyun.com/article/632…

数据结构的堆、栈和操作系统的堆内存、栈内存的区别

kylenxu.github.io/2019/06/17/…