在 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全局对象可以看成一个根节点。 - 然后,垃圾回收器从所有根节点出发,遍历其可以访问到的子节点,并将其标记为活动的,根节点不能到达的地方即为非活动的,将会被视为垃圾。
- 最后,垃圾回收器将会释放所有非活动的内存块,并将其归还给操作系统。
以下几种情况都可以作为根节点:
- 全局对象
- 本地函数的局部变量和参数
- 当前嵌套调用链上的其他函数的变量和参数
Mark-Sweep算法存在一个问题,就是在经历过一次标记清除后,内存空间可能会出现不连续的状态,导致内存不能用
为了解决这种内存碎片的问题,Mark-Compact(标记整理)算法被提了出来,该算法主要就是用来解决内存的碎片化问题的,回收过程中将死亡对象清除后,在整理的过程中,会将活动的对象往堆内存的一端进行移动,移动完成后再清理掉边界外的全部内存。
以上方案垃圾回收会阻碍主线程的执行。
为了减少垃圾回收带来的停顿时间,V8引擎又引入了Incremental Marking(增量标记)的概念,即将原本需要一次性遍历堆内存的操作改为增量标记的方式,先标记堆内存中的一部分对象,然后暂停,将执行权重新交给JS主线程,待主线程任务执行完毕后再从原来暂停标记的地方继续标记,直到标记完整个堆内存。这个理念其实有点像React框架中的Fiber架构,只有在浏览器的空闲时间才会去遍历Fiber Tree执行对应的任务,否则延迟执行,尽可能少地影响主线程的任务,避免应用卡顿,提升应用性能。
得益于增量标记的好处,V8引擎后续继续引入了延迟清理(lazy sweeping)和增量式整理(incremental compaction),让清理和整理的过程也变成增量式的。同时为了充分利用多核CPU的性能,也将引入并行标记和并行清理,进一步地减少垃圾回收对主线程的影响,为应用提升更多的性能。
避免内存泄漏
尽可能少地创建全局变量
手动清除定时器
少用闭包,手动清除闭包
清除DOM引用
采用弱引用
文章学习来着
作者:程序员冷月
链接:juejin.cn/post/684490…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。