垃圾回收 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,此时则有空闲时间做其他事情。