浅析JavaScript垃圾收集

66 阅读4分钟

前言

何谓JavaScript垃圾,很简单,我们创建基元,对象,函数,使用完后不被需要了,但是因为种种原因,得不到释放,一直在内存中占用内存。便是垃圾。例如,我们使用完了一个对象或者函数,但是使用后,一直没有通过置为null来释放。

可达性

JavaScript中内存管理的主要概念是可达性。

简而言之,“可达”值是指可以某种方式访问或使用的值。确保将它们存储在内存中。

  1. 有一组固有的固有值,由于明显的原因而无法删除。
    例如:

    • 当前函数的局部变量和参数。
    • 当前嵌套调用链中其他函数的变量和参数。
    • 全局变量。
  2. 如果其他值可以通过引用或引用链从根访问,则认为该值是可以到达的。最典型的就是闭包。函数内部访问了外部的变量。外部的变量就不会释放。

JavaScript引擎中有一个称为垃圾收集器的后台进程。它监视所有对象,并删除那些变得无法访问的对象。

示例:

let user = {
  name: "John"
};

全局变量"user"引用该对象{name: "John"}。"name"的属性存储一个基元John。

如果的值user被覆盖,则引用丢失:

user = null;

现在对象变得不可及。无法访问它,也没有对其的引用。垃圾收集器将垃圾数据并释放内存。

现在,假设我们将引用从复制user到admin:

let user = {
  name: "John"
};

let admin = user;

现在,如果我们执行相同的操作:

user = null;

然后该对象仍然可以通过admin全局变量访问,因此它在内存中。如果我们也覆盖admin,则可以将其删除。

互连对象

现在是一个更复杂的示例:

function marry(man, woman) {
  woman.husband = man;
  man.wife = woman;

  return {
    father: man,
    mother: woman
  }
}

let family = marry({
  name: "John"
}, {
  name: "Ann"
});

到目前为止,所有对象都是可访问的。

现在让我们删除两个引用:

delete family.father;
delete family.mother.husband;

因此,John现在无法访问,并且将从其内存中删除的所有数据也变得不可访问。

垃圾回收后:

无法到达的岛屿

源对象与上面相同。然后:

family = null;

内存中的图片变为:

显然,John和Ann仍然保持链接,两者都有传入的引用。但这还不够。前一个"family"对象已与根断开连接,不再引用它,因此整个岛将变得不可访问并将被删除。

内部算法

基本的垃圾收集算法称为“标记清除”。

定期执行以下“垃圾收集”步骤:

  • 垃圾收集器扎根并“标记”(记住)它们。
  • 然后,它访问并“标记”来自它们的所有引用。
  • 然后,它访问标记的对象并标记其引用。记住所有访问过的对象,以免将来再次访问同一对象。
  • …依此类推,直到访问了所有可到达的引用(从根开始)。
  • 除标记对象外的所有对象均被删除

例如,让我们的对象结构如下所示:

我们可以清楚地在右侧看到一个“无法到达的岛屿”。

  1. 第一步标记了全局变量上的所有引用的基本类型和引用类型,即为根源:

  2. 然后标记根源的引用:

  3. 现在,在该过程中无法访问的对象被认为是不可访问的,并将被删除:

我们可以想象该过程是从根部依据引用进行查找,标记了所有可到达的对象。未标记的然后被删除。

优化

这就是垃圾收集工作原理的概念。JavaScript引擎进行了许多优化,以使其运行更快且不影响执行。

  1. 分代收集–将对象分为两组:“新的”和“旧的”。许多物体出现,工作并迅速死亡,因此可以积极地清理它们。那些生存时间足够长的人会变得“老”,接受检查的频率降低。
  2. 增量收集–如果有很多对象,并且我们尝试同时遍历并标记整个对象集,可能会花费一些时间,并在执行过程中引入明显的延迟。因此,引擎尝试将垃圾收集分为几部分。然后,分别地执行这些片段。这需要在它们之间进行额外的标记以跟踪更改,但是我们有很多微小的延迟,而不是很大的延迟。
  3. 空闲时间收集–垃圾收集器仅在CPU空闲时尝试运行,以减少对执行的可能影响。