注:

本文参考自
Garbage collection: https://javascript.info/garbage-collection#reachability

在js里内存管理是自动执行且不可见的,我们创建的原始值(primitives),对象,函数等等,这些都会占用内存。
当这些不再被需要的时候会发生什么?JS引擎是如何发现以及清除它们的?

可访问性

可访问性是JS内存管理的主要概念。
简单地说,‘可访问的’值就是以某种方式能被获取和使用的值。它们一定是被存储在内存中的。

  1. 有一些固有的可访问值的基本集合,它们无法被删除

    1. 当前函数的局部变量和参数
    2. 当前嵌套调用链上其他函数的变量和参数
    3. 全局变量
    4. (还有一些其他内部的)

      这些被称之为根(root)

  2. 如果值能被根(root)通过引用或引用链的方式访问到,那么值就被认为是可访问的

比如说被全局变量引用的对象,其属性值引用了另一个对象,那这个对象就是被认定为可访问的。以及它的那些引用同样是可访问的。详细示例如下:
在JS引擎中有一个后台进程被称为垃圾回收机制,它监视着所有的目标并删除那些无法被访问的目标

简单例子

  1. //user变量对一个对象的引用
  2. let user = {
  3. name: "John"
  4. };

image.png
箭头表示一个对象的引用。全局变量‘user’引用了对象{name:’’’john’},对象的name属性存储着一个原始值,它描绘着对象内部。如果user被重新赋值,那么原来的引用将会丢失。

  1. user = null

image.png
这时这个对象变为了不可访问,没有方法去获取到它了。当它不再被引用时,垃圾回收机制将丢弃这个数据释放内存。

双重引用

从user拷贝一份引用给admin

  1. // user 对这个对象的引用
  2. let user = {
  3. name: "John"
  4. };
  5. let admin = user;

image.png
做上面相同的事情:

  1. user = null;

这个对象依旧可以通过admin这个全局变量访问到,所以它仍在内存中。但如果我们也对admin重新赋值,那么这个对象将会被移除。

互联对象

  1. function marry(man, woman) {
  2. woman.husband = man;
  3. man.wife = woman;
  4. return {
  5. father: man,
  6. mother: woman
  7. }
  8. }
  9. let family = marry({
  10. name: "John"
  11. }, {
  12. name: "Ann"
  13. });

marry函数让传入的两个对象互相引用,然后返回一个新的对象包含传入的两个对象
内存结构如下
image.png
当目前为止,所有的对象都是可访问的

  1. delete family.father;
  2. delete family.mother.husband;

image.png
只删除两种引用之一并不能够触发垃圾回收机制删掉这个对象,因为对象仍然是可访问的。但如果两种引用都删除,那么John将不再有传入引用。
image.png
传出引用是不起作用的,只用传入引用能使一个一个对象被访问到。所以Jhon现在是不可访问的,它将会被垃圾回收机制从内存移除同时它所有的数据将无法再被获取。
垃圾回收机制执行后:

image.png

无法访问的数据块(孤岛)

有可能一个互联对象构成的数据块会变得整个无法访问,那么这个数据块将会被从内存移除
源对象同上:

  1. family = null

内存图将变成如下:
image.png
这个例子演示了可访问概念的重要性
当然,John和Ann仍然是连结的,它们都有传入引用,但依旧不足以构成不被垃圾回收机制删除的理由。
它们的前者‘family’对象已经和根(root)失去了连结,这就变得不再有引用了,所以整个数据块就成了孤岛,不能再被访问,所以将会被删除。

内部算法

基础垃圾回收机制算法被称为标清除算法(mark-and-sweep)
下面垃圾回收机制的步骤会定期执行:

  • 垃圾回收机制到根(root)并标记(记住)它们
  • 然后检查并标记它们所有的引用
  • 再检查及标记对象即它们的引用。所有被检查过的对象将会被记住,这样在未来一段时间内就不需要再次去检查它们了。
  • 等等诸如此类,直到每一项至根(root)以来的可访问引用都被检查过。
  • 然后出了被标记的对象以外,其余全都被移除

例如,让我们的对象结构如下:
image.png
我们能清楚地看到不可访问的数据块(孤岛)在右边,现在让我们看标记清除算法是如何处理它的
第一步是标记根(root):
image.png
然后他们的引用将被标记:
image.png
以及引用的引用:
image.png
那些在过程中无法被检查的对象将被认为是不可访问的,然后会被移除
image.png
我们可以想象进程开启后像是从根(root)溅出的一大桶油漆,流经所有引用,标记那些所有可访问到的对象。未被标记的将被移除。

这就是垃圾回收机制的工作机理,JS引擎运用了非常多的优化去使它更快运行且不影响JS代码的执行。

它的一些优化:

  • 世代收集: 对象会被分为两个集合,一个新集合,一个旧集合。很多对象诞生,完成工作后迅速消亡。一些存活的久的对象就会变成‘老对象’,对他的检查就会变少。
  • 增量收集:如果有很多对象,那么我们会立刻尝试遍历和标记整个对象集合,这可能会花费些许时间,在JS代码执行时导致明显的延迟。所以JS引擎尝试将垃圾回收机制分为几个部分执行。那么这些部分就会分别一个一个执行,这要求它们做额外的记录去追踪它们的改变。但我们使用的较小延迟的取代了较大延迟的。
  • 空闲时间收集:垃圾回收器会在CPU空闲时尝试运行,减少在JS代码执行时带去的可能影响

总结

  • 垃圾回收是自动执行的,我们不能强制和阻止它
  • 当对象可访问时,它会保存在内存中
  • 单纯的被引用和被根(root)引用并不相同,一团相互连结的对象也可能整个不可访问(孤岛)