JS的垃圾回收机制(译文)v3

283 阅读6分钟

原文连接:javascript.info/garbage-col…

本文章对原文做了部分删减以及修改

垃圾回收(Garbage collection)

JavaScript 中的内存管理对我们来说是自动且不可见的。我们创建原始数据类型、对象、函数……所有这些值都需要内存。

当不再需要某些值时会发生什么?JavaScript 引擎如何发现并清理它?


可达性(Reachability)

JavaScript 中内存管理的主要概念就是可达性。(应该也是回收机制的主要概念)

简单点说,内存中的值可分为 "可达值(reachable value)""不可达值" 两种,可达值是JS以某种方式可访问到或可用的值,不可达值正相反。

1.有一组基本的可达值,由于显而易见的原因是不能被回收的。

  • 当前执行的函数,以及它的局部变量和参数。
  • 当前嵌套调用链(应该是执行栈?)上的其他函数、以及它们的局部变量和参数
  • 全局变量
  • (还有一些其他的,内部的) 这些可达值称为 "根值(root)"

2.原文的这句话可能有一丢丢误导性,这里的reference的意思可能是reference type,不太清楚作者的原义,希望有能看懂的人帮忙翻译下。

Any other value is considered reachable if it’s reachable from a root by a reference or by a chain of references.

所有能够通过根值向下访问到的值都被认为是可达的,也就是可达值。


举个栗子

例如,如果全局变量中有一个对象,并且该对象有一个字面量为"Yehger"的name属性,则该字面量则被认为是可达的。详细示例如下。

let user = {
    name:"Yehger"
}

如果name的值被改变(name被重新赋值),那么字面量"Yehger"将引用丢失,也就是说JS无法通过根值访问到字面量"Yehger",没有办法访问它,没有对它的引用。垃圾收集器将垃圾数据回收并释放内存。详细示例如下。

let user = {
    name:"Yehger"
}
//重新赋值了属性name,字面量Yehger将被回收
user.name = "no name"

栗子 栗子

假设我们有这样一段代码

let user = {
    name:"Yehger"
}
let admin = user.name
user.name = null

这个时候没有任何值被回收,因为字面量"Yehger"依然能够被admin访问,如果我们将admin的值也覆盖掉,那么字面量"Yehger"将会被回收


复杂一点的栗子

假设我们有这样一段比刚才复杂一点的代码

function Marry(man,woman){
    man.wife = woman
    woman.husband = man
    return {
        father:man,
        mother:woman
    }
}

const family = new Marry({
    name:'John'
},{
    name:'Ann'
})

我们通过实例化Marry方法得到了变量family,此时的family产生的内存结构:

1636017651(1).jpg

到目前为止,所有对象都可以访问。 现在让我们删除两个引用:

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

image.png 删除上面两个引用的其中一个是不够的,字面量"John"仍然可以通过另外一个引用访问到。

所以我们像这样,两种引用方式都删除掉,那么我们就可以看到如上下两张图所示的一样,JS无法通过根值触达"John"了

但似乎"John"依然能够访问到"Ann",不过这都无关紧要,重要的是根值已经无法触达John了。

image.png

回收后的内存结构如下:

image.png

我们还可以将整个family重新赋值

family = null;

image.png

如我之前所说的一样,即便这个对象内部还在相互引用,但这都无关紧要,重要的是根值已经无法触达这个对象了。


内部算法

基本的垃圾收集算法称为 “标记和清除”(mark-and-sweep)

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

  • 垃圾收集器扎根并“标记”(记住)它们。
  • 然后它访问并“标记”来自它们的所有引用。
  • 然后它访问标记的对象并标记它们的引用。所有访问过的对象都会被记住,以免以后访问同一个对象两次。
  • ...依此类推,直到访问每个可访问的(从根)引用。
  • 除标记对象外的所有对象都将被删除。

例如,让我们的对象结构看起来像这样:

image.png

我们可以清楚地看到右侧有一个“无法到达的岛屿”。现在让我们看看“标记和清除”垃圾收集器如何处理它。

第一步标记根:

image.png 然后他们的引用被标记:

image.png ......以及他们向下引用的值

image.png

现在在进程中无法访问的对象被认为是不可访问的,将被删除:

image.png

我们也可以想象这个过程是从根部洒出一大桶油漆,流过所有引用并标记所有可到达的对象。然后删除未标记的那些。

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

  • 分代集合——对象被分成两组:“新的”和“旧的”。许多物体出现,完成它们的工作并快速死亡,它们可以被积极地清理。那些活得足够长的人会变“老”,并且检查频率较低。
  • 增量收集- 如果有很多对象,并且我们尝试一次遍历并标记整个对象集,则可能需要一些时间并在执行中引入明显的延迟。因此引擎尝试将垃圾收集拆分为多个部分。然后这些部分一个一个地分别执行。这需要他们之间进行一些额外的簿记以跟踪更改,但是我们有许多微小的延迟而不是大的延迟。
  • 空闲时间收集——垃圾收集器尝试仅在 CPU 空闲时运行,以减少对执行的可能影响。

原作者的话:垃圾收集算法存在其他优化和风格。尽管我想在这里描述它们,但我不得不推迟,因为不同的引擎实现了不同的调整和技术。而且,更重要的是,随着引擎的发展,事情会发生变化,因此在没有真正需要的情况下“提前”深入研究可能不值得。当然,除非纯粹出于兴趣,否则下面会有一些链接供您参考。

概括

一些我们要知道的主要事项

  • 垃圾收集是自动执行的。我们不能强迫或阻止它。
  • 对象在可访问时保留在内存中。
  • 被引用与可访问(从根)不同:一组相互关联的对象作为一个整体可能变得不可访问(意思就无根值)。

现代引擎实现了先进的垃圾收集算法。

一本通用书籍“垃圾收集手册:自动内存管理的艺术”(R. Jones 等人)涵盖了其中的一些。

如果您熟悉底层编程,有关 V8 垃圾收集器的更详细信息在文章A tour of V8: Garbage Collection 中

V8 博客也不时发布有关内存管理变化的文章。自然地,要学习垃圾收集,您最好通过了解 V8 内部的一般知识并阅读作为 V8 工程师之一工作的Vyacheslav Egorov的博客来做好准备。我的意思是:“V8”,因为它最好被互联网上的文章覆盖。对于其他引擎,很多方法是相似的,但是垃圾收集在很多方面有所不同。

当您需要低级优化时,深入了解引擎非常有用。在您熟悉该语言后,将其作为下一步计划是明智的。