JS垃圾回收

116 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第N天,点击查看活动详情 >>

V8引擎在64位系统下最多只能使用约1.4GB的内存,在32位系统下最多只能使用约0.7GB的内存

为什么要有内存上限

内存大,垃圾多,回收勤, 现成阻塞 -> 页面卡住

Web Wroker 可以创建子线程,但是子线程完全受主线程控制,不能访问浏览器特定的API,例如操作DOM,因此这个新标准并没有改变JS单线程的本质。

栈区域垃圾回收

栈区存放的一般是执行上下文

  • 从逻辑上的垃圾回收: 上下文弹出栈
  • 从实际上的垃圾回收:指向栈顶的指针向下移动,之后入栈的执行上下文直接覆盖 已(逻辑)删除 的上下文

堆区域垃圾回收

常见的GC算法

  1. 引用计数:

    核心思想:就是设置引用计数器,判断当前引用数是否为 0,通过一个引用计数器,当引用关系发生改变的时候,就会修改引用计数器的数字

    缺点:无法回收循环引用的对象,比如 a.test =b , b.test =a

  2. 标记清除:

    核心思想:图的可达性

    缺点:空间碎片化

  3. 标记整理:标记清除的进阶版

  4. 分代回收

V8的垃圾回收策略

V8的垃圾回收策略主要是基于分代式垃圾回收机制,其根据对象的存活时间将内存的垃圾回收进行不同的分代,然后对不同的分代采用不同的垃圾回收算法。

V8的内存结构

  • 新生代(new_space) :大多数的对象开始都会被分配在这里,这个区域相对较小但是垃圾回收特别频繁,该区域被分为两半,一半用来分配内存,另一半用于在垃圾回收时将需要保留的对象复制过来。

  • 老生代(old_space)

    • 新生代中的对象在存活一段时间后就会被转移到老生代内存区,相对于新生代该内存区域的垃圾回收频率较低。
    • 老生代又分为老生代指针区和老生代数据区,前者包含大多数可能存在指向其他对象的指针的对象,后者只保存原始数据对象,这些对象没有指向其他对象的指针。
  • 大对象区(large_object_space) :存放体积超越其他区域大小的对象,每个对象都会有自己的内存,垃圾回收不会移动大对象区。

  • 代码区(code_space) :代码对象,会被分配在这里,唯一拥有执行权限的内存区域。

  • map区(map_space) :存放Cell和Map,每个区域都是存放相同大小的元素,结构简单(这里没有做具体深入的了解,有清楚的小伙伴儿还麻烦解释下)。

新生代

概念 新生代内存是由两个 semispace(半空间) 构成的,内存最大值在 64 位系统和 32 位系统上分别为 32MB 和 16MB,在新生代的垃圾回收过程中主要采用了Scavenge算法。

常规过程:

  1. 它将新生代内存 一分为二,每一个部分的空间称为 semispace
  2. 处于激活状态的区域我们称为 From 空间,未激活(inactive new space)的区域我们称为 To 空间。
  3. 这两个空间中,始终只有一个处于使用状态,另一个处于闲置状态。我们的程序中声明的对象首先会被分配到 From 空间,当进行垃圾回收时,
  4. 如果 From 空间中尚有存活对象,则会被复制到 To 空间进行保存,非存活的对象会被自动回收。当复制完成后,From空间和To空间完成一次角色互换,To空间会变为新的From空间,原来的From空间则变为To空间。
  5. 其中对于From空间的整理是采用 标记整理

对象晋升:将新生代对象移动至老生代对象存储空间

  1. 一轮 GC 操作(Scavenge)后还存活的新生代对象
  2. 当 to 空间使用率超过 25%的时候,因为 To 之后会变成 From 空间给后续的对象进行分配,所以超过25%会认为不太好了进行分配

老生代

概念:采用新的算法Mark-Sweep(标记清除)Mark-Compact(标记整理)

Mark-Sweep(标记清除) 分为 标记 和 清除 两个阶段,在标记阶段会遍历堆中的所有对象,然后标记活着的对象,在清除阶段中,会将死亡的对象进行清除。Mark-Sweep算法主要是通过判断某个对象是否可以被访问到,从而知道该对象是否应该被回收,具体步骤如下:

  • 垃圾回收器会在内部构建一个根列表,用于从根节点出发去寻找那些可以被访问到的变量。比如在JavaScript中,window全局对象可以看成一个根节点。
  • 然后,垃圾回收器从所有根节点出发,遍历其可以访问到的子节点,并将其标记为活动的,根节点(currentNode)不能到达的地方即为非活动的,将会被视为垃圾。
  • 最后,垃圾回收器将会释放所有非活动的内存块,并将其归还给操作系统。

以下几种情况都可以作为根节点:

  1. 全局对象
  2. 本地函数的局部变量和参数
  3. 当前嵌套调用链上的其他函数的变量和参数

Mark-Component(标记整理) 回收过程中将死亡对象清除后,在整理的过程中,会将活动的对象往堆内存的一端进行移动,移动完成后再清理掉边界外的全部内存

前面有提到垃圾回收会阻塞JS线程 -> 增量标记 / 延迟清除 / 增量整理

Incremental Marking(增量标记) :即将原本需要一次性遍历堆内存的操作改为增量标记的方式,先标记堆内存中的一部分对象,然后暂停,将执行权重新交给JS主线程,待主线程任务执行完毕后再从原来暂停标记的地方继续标记,直到标记完整个堆内存。

延迟清理(lazy sweeping)增量式整理(incremental compaction) ,让 清理 和 整理 的过程也变成增量式的。同时为了充分利用多核CPU的性能,也将引入 并行标记 和 并行清理 ,进一步地减少垃圾回收对主线程的影响,为应用提升更多的性能。

BTW:类似于 Fiber 的实现