微信公众号:[前端一锅煮]
一点技术、一点思考。
- 栈空间
- 堆空间
- 新生代内存回收
- 老生代内存回收
- 标记革除、标记整顿、增量标记
JavaScript 引擎的内存空间次要分为栈和堆。
V8 的垃圾回收策略次要基于分代式垃圾回收机制。依照对象的存活工夫将内存的垃圾回收进行不同分代,而后别离对不同分代的内存应用最适宜的算法。次要分为新生代和老生代,有标记革除、标记整顿、增量标记等办法。
栈空间
栈是长期存储空间,次要存储局部变量和函数调用。
根本类型赋值(Number, Boolean, String, Null, Undefined, Symbol, BigInt),零碎会为新的变量在栈内存中调配一个新值。
援用类型赋值,零碎会为新的变量在栈内存中调配一个值,这个值仅仅是指向同一个对象的援用,和原对象指向的都是堆内存中的同一个对象。
对于函数,解释器创立了”调用栈“来记录函数的调用过程。每调用一个函数,解释器就把该函数增加进调用栈,解释器会为被增加进来的函数创立一个栈帧(用来保留函数的局部变量以及执行语句)并立刻执行。如果正在执行的函数还调用了其余函数,新函数会持续被增加进入调用栈。函数执行实现,对应的栈帧立刻被销毁。
两种查看调用栈的办法
应用 console.trace() 向 web 控制台输入一个堆栈跟踪。
浏览器开发者工具进行断点调试。
栈溢出
栈尽管很轻量,在应用时创立,应用完结后销毁,然而不是能够有限增长的,被调配的调用栈空间被占满时,就会引起”栈溢出“的谬误。
(function foo() {foo()
})()
Maximum call stack size exceeded.
为什么根本数据类型存储在栈中,援用数据类型存储在堆中?
JavaScript 引擎须要用栈来维护程序执行期间的上下文的状态,如果栈空间大了的话,所有数据都寄存在栈空间外面,会影响到上下文切换的效率,进而影响整个程序的执行效率。
堆空间
堆空间存储的数据比较复杂,大抵能够划分为 5 个区域:
- 新生代内存区(new space):新生代内存区会被划分为两块,别离是 from space 和 to space(具体有什么用下文会说),64 位零碎下默认 32MB,32 位零碎下默认 16MB,通常新创建的对象会先放入 from 中。
- 老生代内存区(old space):较为长久的保留对象,分为两个区域 old pointer space 和 old data space 别离用来寄存 GC 后还存活的指针信息和数据信息,64 位零碎下能应用约 1.4GB,32 位零碎下能应用约 0.7GB。
- 大对象区(large object space):这里寄存体积超过其余区大小的对象,次要为了防止大对象的拷贝,应用该空间专门存储大对象。
- 单元区、属性单元区、Map 区(Cell space、property cell space、map space):Map 空间寄存对象的 Map 信息也就是暗藏类 (Hiden Class)最大限度为 8MB;每个 Map 对象固定大小,为了疾速定位,所以将该空间独自进去。
- 代码区 (code Space):次要寄存代码对象,最大限度为 512MB,也是惟一领有执行权限的内存。
新生代内存是长期调配的内存,存活工夫短,老生代内存是常驻内存,存活工夫长。
新生代内存回收
新生代内存中的垃圾回收次要通过 Scavenge 算法进行,具体实现时次要采纳 Cheney 算法。
Cheney 将内存空间一分为二,一块叫做 From 正在应用的内存,另一块叫做 To 目前闲置的内存。
Scavenge GC 算法:
- 存活的对象从 from space 转移到 to space
- 清空 from space
- from space 与 to space 调换
- 实现一次新生代 GC
简而言之,在垃圾回收的过程中,将存活对象在两个空间之间进行复制。
Scavenge 是典型的就义空间换取工夫的算法,毛病是只能应用堆内存中的一半,这是由划分空间和复制机制所决定的。但因为只复制存活的对象,并且对于生命周期短的场景存活对象只占少部分,所以它在工夫效率上有优异的体现。
老生代内存回收
V8 在老生代中次要采 用了 Mark-Sweep 和 Mark-Compact 相结合的形式进行垃圾回收。
降职
当一个对象通过屡次复制仍然存在时,它将会被认为是生命周期较长的对象,这种对象会被移到老生代中,采纳新的算法进行治理,这种挪动称之为“升级”。
对象升级的条件次要有两个:
曾经经验过一次 Scavenge 回收
To(闲置内存) 空间的内存不足 75%
标记革除(Mark-Sweep)
标记革除,分为标记和革除两个阶段。在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的革除阶段中,只革除没有被标记的对象。能够看出,Scavenge 中只复制活着的对象,而 Mark-Sweep 只清理死亡对象。
标记革除最大的问题是在进行一次标记革除回收后,内存空间会呈现不间断的状态。这种内存碎片会对后续的内存调配造成问题,因为很可能呈现须要调配一个大对象的状况,这时所有的碎片空间都无奈实现此次调配,就会提前触发垃圾回收,而这次回收是不必要的。为了解决标记革除的内存碎片问题,标记整顿(Mark-Compact)被提出来。
标记整顿(Mark-Compact)
Mark-Compact 是标记整顿的意思,在 Mark-Sweep 的根底上演变而来。
标记整顿看待未存活对象不是立刻回收,而是将存活对象挪动到一边,而后间接清掉端边界以外的内存。
增量标记
为了避免出现 JavaScript 应用程序与垃圾回收器看到的不统一的状况,进行垃圾回收的时候,都须要将正在运行的程序停下来,期待垃圾回收执行实现之后再回复程序的执行,这种景象称为“全进展”。如果须要回收的数据过多,那么全进展的时候就会比拟长,会影响其余程序的失常执行。
为了防止垃圾回收工夫过长影响其余程序的执行,V8 将标记过程分成一个个小的子标记过程,同时让垃圾回收和 JavaScript 应用逻辑代码交替执行,直到标记阶段实现。咱们称这个过程为增量标记算法。
艰深了解,就是将本来一口气实现的标记工作分为了很多小的局部去实现, 每实现一个小工作就停一会, 让 js 逻辑执行一会, 而后再继续执行上面的局部。
总结
从 V8 垃圾回收机制能够看到,垃圾回收是一件十分耗时的事件, 以 1.5GB 的垃圾回收堆内存为例,V8 做一次小的垃圾回收须要 50ms 以上,做一次非增量式的垃圾回收甚至要 1s 以上,所以要做限度。
新生代设计为一个较小的内存空间是正当的,而老生代空间过大对于垃圾回收并无特地意义。V8 对内存限度的设置对于 Chrome 浏览器这种每个选项卡页面应用一个 V8 实例而言,内存的应用是入不敷出了。对于 Node 编写的服务器端来说,内存限度也并不影响失常场景下的应用。然而对于 V8 的垃圾回收特点和 js 在单线程上的执行状况,垃圾回收是影响性能的因素之一。想要高性能的执行效率,须要留神让垃圾回收尽量少地进行,尤其是全堆垃圾回收。
其余
vue-cli 打包内存溢出,批改内存限度
node_modules/.bin
vue-cli-service
#!/usr/bin/env node --max_old_space_size=4096
调整老生代内存限度,单位 mb
node --max-old-space-size=2048 build/build.js
调整新生代内存限度,单位 kb
node --max-new-space-size=2048 build/build.js