js堆栈 以及垃圾回收机制总结

157 阅读7分钟

在 JavaScript 中,堆(Heap)和栈(Stack)是两个常用的内存区域,用于存储不同类型的数据和执行上下文。

堆(Heap)内存

  • 堆内存是用来存储动态分配的对象、函数和闭包等引用类型数据的区域。
  • 在堆中分配的内存需要手动释放或由垃圾回收器自动回收。
  • JavaScript 中的对象和数组都分配在堆内存中,并且可以通过引用进行访问和操作。

栈(Stack)内存

  • 栈内存是用来存储基本类型值、函数调用以及执行上下文的区域。
  • 每当函数被调用时,都会创建一个新的执行上下文,并将其推入栈内存中。当函数执行完毕后,对应的执行上下文会出栈并销毁。
  • 栈遵循先进后出的原则,因此最后进栈的执行上下文首先被执行和销毁。栈内存的大小通常较小,受到限制。

当我们编写 JavaScript 代码时,变量的声明和基本类型的赋值通常发生在栈内存中。而在堆内存中,通过引用访问和操作对象、数组等复杂类型的数据。

JS单线程的主要用途是与用户交互以及操作DOM,那么这也决定了其作为单线程的本质,单线程意味着执行的代码必须按顺序执行,在同一时间只能处理一个任务.

垃圾回收机制:垃圾回收本身也是一件非常耗时的操作,假设V8的堆内存为1.5G,那么V8做一次小的垃圾回收需要50ms以上,而做一次非增量式回收甚至需要1s以上,可见其耗时之久,而在这1s的时间内,浏览器一直处于等待的状态,同时会失去对用户的响应,如果有动画正在运行,也会造成动画卡顿掉帧的情况,严重影响应用程序的性能。因此如果内存使用过高,那么必然会导致垃圾回收的过程缓慢,也就会导致主线程的等待时间越长,浏览器也就越长时间得不到响应。

基于以上两点,V8引擎为了减少对应用的性能造成的影响,采用了一种比较粗暴的手段,那就是直接限制堆内存的大小

V8的垃圾回收策略主要是基于分代式垃圾回收机制,其根据对象的存活时间将内存的垃圾回收进行不同的分代,然后对不同的分代采用不同的垃圾回收算法。

V8的内存结构

  • 新生代(new_space):大多数的对象开始都会被分配在这里,这个区域相对较小但是垃圾回收特别频繁,该区域被分为两半,一半用来分配内存,另一半用于在垃圾回收时将需要保留的对象复制过来。

  • 老生代(old_space):新生代中的对象在存活一段时间后就会被转移到老生代内存区,相对于新生代该内存区域的垃圾回收频率较低。老生代又分为老生代指针区老生代数据区,前者包含大多数可能存在指向其他对象的指针的对象,后者只保存原始数据对象,这些对象没有指向其他对象的指针。

  • 大对象区(large_object_space):存放体积超越其他区域大小的对象,每个对象都会有自己的内存,垃圾回收不会移动大对象区。

  • 代码区(code_space):代码对象,会被分配在这里,唯一拥有执行权限的内存区域。

  • map区(map_space):存放Cell和Map,每个区域都是存放相同大小的元素,结构简单(这里没有做具体深入的了解,有清楚的小伙伴儿还麻烦解释下)。

新生代

在新生代的垃圾回收过程中主要采用了Scavenge算法(主要采用了Cheney算法) 它将新生代内存一分为二,每一个部分的空间称为semispace,也就是我们在上图中看见的new_space中划分的两个区域,其中处于激活状态的区域我们称为活跃空间(存放变量对象),未激活的区域我们称为静置空间(没有存放变量对象)。活跃空间的变量确定存活后会移入静置空间,然后将活跃空间清空内存,这时候静置空间变成活跃空间,活跃空间变成静置空间。

当新生代中的 对象经历过一次Scavenge算法 并且 静置空间的内存占比是否已经超过25%

老生代

之前用 引用计数算法,在循环引用的场景下会导致此旧变量无法被回收,导致内存泄漏。 转而采用新的Mark-Sweep(标记清除)Mark-Compact(标记整理)算法。

Mark-Sweep算法主要是通过判断某个对象是否可以被访问到,从而知道该对象是否应该被回收,具体步骤如下:

  • 垃圾回收器会在内部构建一个根列表,用于从根节点出发去寻找那些可以被访问到的变量。比如在JavaScript中,window全局对象可以看成一个根节点。
  • 然后,垃圾回收器从所有根节点出发,遍历其可以访问到的子节点,并将其标记为活动的,根节点不能到达的地方即为非活动的,将会被视为垃圾。
  • 最后,垃圾回收器将会释放所有非活动的内存块,并将其归还给操作系统。

以下几种情况都可以作为根节点:

  1. 全局对象
  2. 本地函数的局部变量和参数
  3. 当前嵌套调用链上的其他函数的变量和参数

Mark-Sweep算法存在一个问题,就是在经历过一次标记清除后,内存空间可能会出现不连续的状态,导致内存不能用 为了解决这种内存碎片的问题,Mark-Compact(标记整理)算法被提了出来,该算法主要就是用来解决内存的碎片化问题的,回收过程中将死亡对象清除后,在整理的过程中,会将活动的对象往堆内存的一端进行移动,移动完成后再清理掉边界外的全部内存。 以上方案垃圾回收会阻碍主线程的执行。

为了减少垃圾回收带来的停顿时间,V8引擎又引入了Incremental Marking(增量标记)的概念,即将原本需要一次性遍历堆内存的操作改为增量标记的方式,先标记堆内存中的一部分对象,然后暂停,将执行权重新交给JS主线程,待主线程任务执行完毕后再从原来暂停标记的地方继续标记,直到标记完整个堆内存。这个理念其实有点像React框架中的Fiber架构,只有在浏览器的空闲时间才会去遍历Fiber Tree执行对应的任务,否则延迟执行,尽可能少地影响主线程的任务,避免应用卡顿,提升应用性能。

得益于增量标记的好处,V8引擎后续继续引入了延迟清理(lazy sweeping)增量式整理(incremental compaction),让清理和整理的过程也变成增量式的。同时为了充分利用多核CPU的性能,也将引入并行标记并行清理,进一步地减少垃圾回收对主线程的影响,为应用提升更多的性能。

避免内存泄漏

尽可能少地创建全局变量 手动清除定时器 少用闭包,手动清除闭包 清除DOM引用 采用弱引用

文章学习来着

作者:程序员冷月
链接:juejin.cn/post/684490…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。