JavaScript 中的活跃度是什么?

Mer*_*sov 5 javascript garbage-collection weak-references

为了研究 JavaScript GC 的复杂性,我深入研究了其中的内容(即 ECMAScript 规范)。我发现,只要一个物体被认为是“活的”,就不应该被收集。活性本身定义如下:

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

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

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

对于某些对象集S,关于S 的假设的 WeakRef 不经意执行是这样一种执行:其所指对象是S的元素的WeakRef的抽象操作WeakRefDeref始终返回undefined

WeakRefDerefWeakRef当其undefined所指对象已被收集时返回。我的理解是否正确,这里暗示所有组成的对象都S应该被收集?因此,未来假设的 WeakRef-oblivious 执行的概念是,仍然存在一个对象, 的一个元素S,尚未被某些 收集和观察WeakRef

这一切对我来说仍然毫无意义。我希望得到一些样品。

Ber*_*rgi 2

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

\n
\n

JavaScript 中的活跃度是什么?

\n
\n

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

\n

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

\n

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

\n

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

\n
\n

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

\n
\n

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

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

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

\n

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

\n

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

\n

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

\n
\n

举例时间!

\n
\n

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

\n
\n

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

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

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

\n

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

\n
{\n    const o = {};\n    const w = new WeakRef(o);\n    setTimeout(() => {\n        console.log(w.deref() && w.deref() === o ? "kept" : "cleared")\n    }, 1000);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

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

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

\n

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

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

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

\n