垃圾回收 GC 
内存空间 
JS 引擎的内存空间主要分为栈内存和堆内存,在创建数据时会自动进行了分配内存,并且在不使用它们时 自动释放内存。
栈内存 
栈是轻量的临时存储空间,主要存储局部变量和函数调用。
- 基本类型数据保存在栈内存。
- 引用类型数据的变量指针:引用类型数据保存在堆内存中,但指向它的变量指针数据保存在栈内存。
- 函数调用栈,解释器创建了调用栈来记录函数的调用过程。- 每调用一个函数,解释器就可以把该函数添加进调用栈, 解释器会为被添加进来的函数创建一个栈帧(用来保存函数的局部变量以及执行语句)并立即执行。 如果正在执行的函数还调用了其他函数,新函数会继续被添加进入调用栈。 函数执行完成,对应的栈帧立即被销毁。 - 查看调用栈: 使用浏览器开发者工具进行 - 断点调试或者使用- console.trace()向 Web 控制台输出一个堆栈跟踪.
堆内存 
堆内存的数据比较复杂,大致划分为 5 个区域:
- 代码区 CodeSpace:这是即时编译器(JIT)存储已经编译的代码块的地方。这是唯一可执行内存的空间(尽管代码可能被分配到大型对象空间(Large object space),那也是可以执行的)。
- 单元空间 CellSpace、属性单元空间 PropertyCellSpace、映射空间 Map Space:分别存放 Cell,PropertyCell 和 Map。它们包含的对象大小相同且类型有限制,可以简化回收工作。
- 大对象区 LargeObjectSpace:大于其他空间大小限制的对象存放在这里。每个对象都有自己的内存区域,这里的对象不会被垃圾回收器移动。
- 新生代 NewSpace: 新对象存活的地方,这些对象的生命周期都很短。
- 老生代 OldSpace: 老生代内存是常驻内存,存活时间长
JS 为什么要使用栈和堆? 
JS 引擎需要用栈来维护程序执行期间的上下文的状态,如果栈空间大了的话,所有数据都存放在栈空间里面,会影响到上下文切换的效率,进而影响整个程序的执行效率。
存活对象 
识别存活内存对象(JS 对象、作用域等)的机制:引用计数和可达性分析等。
一、引用计数 
一个对象有访问另一个对象的权限(隐式或者显式)就是存活对象。
例如:一个 JS 对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
二、可达性 
找出所有的根引用 (全局变量等),一个对象能从根引用上被直接或间接访问到就是存活对象,
内存管理 
在创建变量时自动分配内存,并且在不使用它们时 自动释放内存,内存管理的过程称为垃圾回收。
引用计数算法 
使用引用计数查找非存活对象(没有引用指向该对象),对象将被垃圾回收机制回收。
🐞 缺陷是无法处理循环引用的对象
标记-清除算法 
使用可达性分析查找存活对象,然后清除所有非存活内存对象。
🐞 缺陷是无法从根对象查询到的对象都将被清除
复制算法 
- 把内存分为两部分: FromSpace 和 ToSpace,新数据先在 FromSpace 进行分配;
- 当 FromSpace 被占满,GC 将标记并复制存活对象到 ToSpace(内存整理,避免碎片产生);
- 复制完成后清空 FromSpace,将 FromSpace 和 ToSpace 进行角色互换。
🐞 缺陷是活动内存空间只能使用一半
V8 内存管理 
现代垃圾回收算法是 根据对象的存活时间将内存垃圾进行分代实行不同的回收算法。
- 新生代内存中的对象存活时间较短
- 老生代内存中代对象存活时间较长或是常驻内存
新生代内存回收 
新生代是指刚刚被创建的 JS 对象, 采用 Scavenge 算法(复制-晋升)。 使用 复制算法 进行内存管理。 但当一个对象多次复制后依然处于存活状态,则认为其是长期存活对象, 此时将发生晋升,将该对象移动到老生代内存中,采用新的算法进行管理。
- 晋升条件:一个对象多次复制后依然处于存活状态
- 晋升条件:ToSpace 的内存使用占比超过限制
老生代内存回收 
老生代内存中非存活对象占少数,内存回收采用的是标记清除和标记整理结合的方式。
- 标记-清除算法(Mark-Sweep)- 遍历所有的对象并标记存活对象,在标记完成后清除所有未标记的对象。 - 🐞 遍历操作性能较低,清除操作导致堆内存碎片化。 
- 标记-整理算法(Mark-Compact)- 同样会先遍历所有的对象并标记存活对象, 然后将所有存活的对象整理移动到内存空间的一端,并清空移动后的另一端的内存空间。 - 🐞 整理解决堆内存碎片化的问题,整理过程的性能比 - 标记-清除算法要低, 造成应用执行全暂停。
优化 GC 的全暂停时间 
新生代内存的垃圾回收对应用执行影响不大,但是老生代内存由于存活对象较多,造成的全停顿影响非常大。
垃圾回收时需要暂停应用执行逻辑,待垃圾回收机制结束后再恢复应用执行逻辑,该行为称为 全暂停 STW。
V8 为了优化 GC 的全暂停时间,还引入了增量标记、并发标记、并行标记、增量整理、并行清理、延迟清理等方式。
- 并行GC是开多个辅助线程分担 GC 的事情
- 增量GC是将 GC 工作进行拆分,并在主线程中间歇的分步执行。
- 并发GC是指 GC 在后台运行,不再在主线程运行。该做法会避免 STW 现象。
- 空闲GC是指 Chrome 中动画的渲染大约是 60 帧(每帧约 16ms),如果当前渲染所花费时间每达到 16.6ms,此时则有空闲时间做其他事情。