Skip to content

垃圾回收 GC

内存空间

JS 引擎的内存空间主要分为栈内存堆内存,在创建数据时会自动进行了分配内存,并且在不使用它们时 自动释放内存

栈内存

栈是轻量的临时存储空间,主要存储局部变量和函数调用。

  1. 基本类型数据保存在栈内存。

  2. 引用类型数据的变量指针:引用类型数据保存在堆内存中,但指向它的变量指针数据保存在栈内存。

  3. 函数调用栈,解释器创建了调用栈来记录函数的调用过程。

    每调用一个函数,解释器就可以把该函数添加进调用栈, 解释器会为被添加进来的函数创建一个栈帧(用来保存函数的局部变量以及执行语句)并立即执行。 如果正在执行的函数还调用了其他函数,新函数会继续被添加进入调用栈。 函数执行完成,对应的栈帧立即被销毁。

    查看调用栈: 使用浏览器开发者工具进行 断点调试或者使用 console.trace() 向 Web 控制台输出一个堆栈跟踪.

堆内存

堆内存的数据比较复杂,大致划分为 5 个区域:

  • 代码区 CodeSpace:这是即时编译器(JIT)存储已经编译的代码块的地方。这是唯一可执行内存的空间(尽管代码可能被分配到大型对象空间(Large object space),那也是可以执行的)。
  • 单元空间 CellSpace、属性单元空间 PropertyCellSpace、映射空间 Map Space:分别存放 Cell,PropertyCell 和 Map。它们包含的对象大小相同且类型有限制,可以简化回收工作。
  • 大对象区 LargeObjectSpace:大于其他空间大小限制的对象存放在这里。每个对象都有自己的内存区域,这里的对象不会被垃圾回收器移动。
  • 新生代 NewSpace: 新对象存活的地方,这些对象的生命周期都很短。
  • 老生代 OldSpace: 老生代内存是常驻内存,存活时间长

JS 为什么要使用栈和堆?

JS 引擎需要用栈来维护程序执行期间的上下文的状态,如果栈空间大了的话,所有数据都存放在栈空间里面,会影响到上下文切换的效率,进而影响整个程序的执行效率。

存活对象

识别存活内存对象(JS 对象、作用域等)的机制:引用计数可达性分析等。

一、引用计数

一个对象有访问另一个对象的权限(隐式或者显式)就是存活对象。

例如:一个 JS 对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。

二、可达性

找出所有的根引用 (全局变量等),一个对象能从根引用上被直接或间接访问到就是存活对象,

内存管理

在创建变量时自动分配内存,并且在不使用它们时 自动释放内存,内存管理的过程称为垃圾回收

引用计数算法

使用引用计数查找非存活对象(没有引用指向该对象),对象将被垃圾回收机制回收。

🐞 缺陷是无法处理循环引用的对象

标记-清除算法

使用可达性分析查找存活对象,然后清除所有非存活内存对象。

🐞 缺陷是无法从根对象查询到的对象都将被清除

复制算法

  1. 把内存分为两部分: FromSpace 和 ToSpace,新数据先在 FromSpace 进行分配;
  2. 当 FromSpace 被占满,GC 将标记并复制存活对象到 ToSpace(内存整理,避免碎片产生);
  3. 复制完成后清空 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,此时则有空闲时间做其他事情。

参考资料

༼ つ/̵͇̿̿/’̿’̿ ̿ ̿̿ ̿̿◕ _◕ ༽つ/̵͇̿̿/’̿’̿ ̿ ̿̿ ̿̿ ̿̿