首页 > 解决方案 > JavaScript 中的活力是什么?

问题描述

为了检查 JavaScript GC 的复杂性,我深入研究了杂草(即 ECMAScript 规范)。我发现一个对象只要被认为是“活的”,就不应该被收集。并且liveness本身定义如下:

在评估期间的任何时候,如果满足以下任一条件,则认为一组对象S活动的:

  • S中的任何元素都包含在任何代理的[[KeptAlive]]列表中。
  • 存在一个关于S的有效的未来假设 WeakRef-oblivious 执行,它观察S中任何对象的 Object 值。

一旦创建了特殊对象,该[[KeptAlive]]列表就会附加一个对象,该对象WeakRef(弱)引用它,并在当前同步作业停止后清空。但是,对于WeakRef-oblivious execution,我无法理解它是什么:

对于某些对象集S,关于S的假设的 WeakRef-oblivious 执行是这样一种执行,即其所指对象是S的元素的WeakRef的抽象操作WeakRefDeref总是返回undefined

WeakRefDeref的一个WeakRef返回undefined时,它的所指对象已经被收集。我是否理解正确,这里暗示S应该收集所有组成的对象?所以未来假设的 WeakRef-oblivious 执行的概念是,仍然有一个对象,一个 的元素,它还S没有被一些人收集和观察WeakRef

这一切对我来说仍然毫无意义。我会很感激一些样品。

标签: javascriptgarbage-collectionweak-references

解决方案


让我们忽略形式化但不完整的定义。我们在该部分的非规范性注释中找到了实际含义。1

JavaScript 中的活力是什么?

WeakRef活跃度是保证哪个引擎不能为空的下限(注 6)。因此,活动(组)对象是那些不能被垃圾收集的对象,因为它们仍将被程序使用。

但是,一组对象的活跃度并不意味着必须保留该集合中的所有对象。这意味着集合中的某些对象仍将被程序使用,并且活动集(作为一个整体)不能被垃圾收集。这是因为该定义在垃圾收集器执行算法2中以否定形式使用:在任何时候,如果一组对象S存在,ECMAScript 实现可能会3 […] 原子地 [删除它们]。换句话说,如果一个实现选择一个非实时集合S来清空 WeakRefs,它必须清空其中所有对象的 WeakRefsS同时(注2)。

查看单个对象,如果至少有一个包含它们的非活动集,我们可以说它们不是活动的(垃圾收集);相反,如果包含它的每组对象都是活动的,则我们说单个对象是活动的(注 3)。有点奇怪,因为“活动的对象集”基本上被定义为“其中任何一个都是活动的对象的集合”,但是个体的活动性始终是“相对于集合S”,即这些对象是否可以是垃圾-一起收集。

1:这绝对是整个规范中注释与内容比率最高的部分。
2:强调我的3:从目标
的第一段开始:“本规范不保证任何对象都会被垃圾收集。不活动的对象可能会在很长一段时间后被释放,或者根本不会被释放。对于因此,本规范在描述垃圾收集触发的行为时使用了术语“可能” 。


现在,让我们试着理解这个定义。

在评估期间的任何时候,如果满足以下任一条件,S则认为一组对象是 活动的:

  • 中的任何元素S都包含在任何代理的[[KeptAlive]]列表中。
  • 存在一个有效的未来假设的 WeakRef-oblivious 执行,S它观察到任何对象的 Object 值S

第一个条件很清楚。代理[[KeptAlive]]列表表示要保持活动状态的对象列表,直到当前 Job结束。在同步运行结束后被清除,并且4的注释提供了对意图的进一步洞察:如果 [ WeakRefDeref ] 返回的Object 不是,那么在 ECMAScript 代码的当前执行完成之前,不应对该对象进行垃圾收集完全的。WeakRef.prototype.dereftargetundefinedtarget

然而,第二个条件,哦,好吧。“有效”、“未来执行”和“观察对象值”的含义并没有很好的定义。上面的第二个条件想要捕捉的直觉是,如果一个对象的身份可以通过非 WeakRef 手段(注 2)观察到,那么它就是活的,啊哈。据我了解,“执行”是代理执行 JavaScript 代码以及在此期间发生的操作。如果它符合 ECMAScript 规范,则它是“有效的”。如果它从程序的当前状态开始,它就是“未来”。
可以通过观察对象之间的严格相等比较或观察对象被用作 Map 中的键来观察对象的身份(注释 4),我假设注释仅给出示例,“对象值”表示“身份”。重要的是代码是否关心是否使用了特定对象,所有这些只有在执行结果是可观察到的情况下(即在不改变程序的结果/输出的情况下无法优化)5 .
要通过这些方式确定对象的活跃度,需要测试所有可能的未来执行,直到对象不再可观察。因此,这里定义的活跃度是不可判定的6。在实践中,引擎使用保守的近似值,例如可达性7(注 6),但请注意对更先进的垃圾收集器的研究正在进行中。

现在来看有趣的一点:是什么让执行“假设 WeakRef-oblivious 相对于一组对象S”?S这意味着在所有对象的 WeakRefs都已被清除的假设下执行8。我们假设在未来的执行过程中,其所指对象是一个元素的a的抽象操作WeakRefDeref总是返回WeakRefSundefined(def),然后回溯它是否仍然可能观察到集合中的一个元素。如果在所有对它们的弱引用被清除后,没有一个对象可以被观察到,它们可能会被垃圾回收。除此以外,S被认为是活的,对象不能被垃圾收集,对它们的弱引用不能被清除。

4:请参阅整个注释以获取示例。有趣的是,new WeakRef(obj)构造函数也添加obj[[KeptAlive]]列表中。
5:不幸的是,根据这个非常有趣的 es-discourse 线程,“构成‘观察’的概念故意含糊不清” 。
6:虽然指定不可判定的属性似乎没用,但实际上并非如此。指定一个更差的近似值,例如所说的可达性,将排除一些在实践中可能的优化,即使不可能实现一个通用的 100% 优化器。死代码消除的情况与此类似。
7:指定可达性的概念实际上比描述活性要复杂得多。请参阅注 5,其中给出了通过内部槽和规范类型字段可访问对象但仍应进行垃圾收集的结构示例。
8:另请参阅提案中的 issue 179相应的 PR,了解为什么要引入对象集。


示范时间!

我很难认识到几个对象的活跃度如何相互影响。

WeakRef-obliviousness 和 liveness 一起捕获了 WeakRef 本身不会使对象保持活动状态的概念(注 1)。这几乎就是 WeakRef 的目的,但还是让我们看一个例子:

{
    const o = {};
    const w = new WeakRef(o);
    t = setInterval(() => {
        console.log(`Weak reference was ${w.deref() ? "kept" : "cleared"}.`)
    }, 1000);
}

(您可以在控制台中运行它,然后强制垃圾收集,然后clearInterval(t);

[第二个概念是]活跃的循环并不意味着一个对象是活跃的(注1)。这个有点难展示,但请看这个例子:

{
    const o = {};
    const w = new WeakRef(o);
    setTimeout(() => {
        console.log(w.deref() && w.deref() === o ? "kept" : "cleared")
    }, 1000);
}

在这里,我们清楚地观察到 的身份o。所以它一定是活的?仅当未清除wthat 持有o时,否则… === o不评估。因此(包含的集合)的活跃度o取决于其自身,循环推理,并且实际上允许聪明的垃圾收集器收集它,而不管闭包如何。

具体来说,如果确定obj's活跃度取决于确定另一个 WeakRef 所指对象的活跃度,obj2,obj2的活跃度不能假设活跃度obj's,这将是循环推理(注 1)。让我们尝试用两个相互依赖的对象来做一个例子:

{
    const a = {}, b = {};
    const wa = new WeakRef(a), wb = new WeakRef(b);
    const lookup = new WeakMap([[a, "b kept"], [b, "a kept"]]);
    setTimeout(() => {
        console.log(wa.deref() ? lookup.get(b) : "a cleared");
        console.log(wb.deref() ? lookup.get(a) : "b cleared");
    }, 1000);
}

WeakMap主要用作观察两个对象的身份的东西。在这里,如果a被保留,那么wa.deref()将返回它,b被观察到;如果b被保留,那么wb.deref()将返回它,a被观察到。它们的活力相互依赖,但我们不能做循环推理。垃圾收集器可以同时清除两者wawb但不能只清除其中一个。

Chrome 目前确实通过闭包检查可访问性,因此上述代码段不起作用,但我们可以通过在对象之间引入循环依赖来删除这些引用:

{
    const a = {}, b = {};
    a.b = b; b.a = a;
    const wa = new WeakRef(a), wb = new WeakRef(b);
    const lookup = new WeakMap([[a, "b kept"], [b, "a kept"]]);
    t = setInterval(() => {
        console.log(wa.deref() ? lookup.get(wa.deref().b) : "a cleared");
        console.log(wb.deref() ? lookup.get(wb.deref().a) : "b cleared");
    }, 1000);
}

对我来说,注释 2(WeakRef-obliviousness 是在对象集合而不是单个对象上定义的,以解释循环。如果它是在单个对象上定义的,那么即使仅观察到它的 Object 值,循环中的对象也将被认为是活动的通过循环中其他对象的 WeakRefs。)似乎说的是完全相同的事情。引入该注释是为了修复liveness的定义以处理循环,该问题还包括一些有趣的示例。


推荐阅读