v8如何处理闭包中的堆栈分配变量?

all*_*evo 2 javascript closures memory-management v8

我读了很多文章说“v8 使用堆栈来分配像数字这样的基元”。我还准备了有关 CG 仅适用于堆分配的信息。但是,如果我将堆栈分配的变量与闭包结合起来,那么谁来释放堆栈分配的变量呢?

例如:

function foo() {
    const b = 5;

    return function bar(x) {
        return x * b
    }
}

// This invocation allocate in the stack the variable `b`
// in the head the code of `bar`
const bar = foo()
// here the `b` should be freed

// here `b` is used, so should not be free
bar()
Run Code Online (Sandbox Code Playgroud)

怎么运行的?函数如何bar指向bifb存在于堆栈中?这里是怎样[[Environment]]建造的?

jmr*_*mrk 7

(V8 开发人员在此。)

我不知道“基元是在堆栈上分配的”这个神话从何而来。这通常是错误的:JavaScript 中的常规情况是所有内容都在堆上分配,无论是否原始。

可能存在特定于实现的特殊情况,其中某些堆分配可以被优化和/或被堆栈分配替换,但这是例外,而不是规则;而且它永远不会直接观察到(即永远不会改变行为,只会改变性能),因为这是所有内部优化的一般规则。

为了更深入地研究,我们需要区分两个概念:变量本身和它们指向的事物。

变量可以被认为是一个指针。换句话说,它本身并不是分配对象的“容器”或“空间”;而是分配对象的“容器”或“空间”。相反,它是一个指向在其他地方分配的对象的引用。所有变量都具有相同的大小(1 个指针),它们指向的对象的大小可能相差很大。一个说明性的结果是,随着时间的推移,同一个变量可以指向不同的事物,例如,您可以在数组上进行循环,依次element = array[i]指向每个数组元素。
在现代高性能 JS 引擎中,函数局部变量通常存储在堆栈中(无论它们指向什么!)。那是因为这既快速又方便。因此,虽然这在技术上仍然是所有内容都在堆上分配的规则的特定于实现的异常,但它是一个相当常见的异常。
正如您所观察到的,如果变量需要在创建它们的函数中生存下来,则将变量存储在堆栈上是行不通的。因此,JavaScript 引擎执行分析过程来找出嵌套闭包引用了哪些变量,并立即将这些变量存储在堆上,以便让它们在需要时一直存在。
如果一个更喜欢简单性而不是性能的引擎选择始终将所有变量存储在堆上,那么它就不必区分几种情况,我不会感到惊讶。

关于变量指向的值:无论其类型或原始性如何,该值始终位于堆上(规则有例外,请参见下文)。
var a = true-->true在堆上。
var b = "hello"-->"hello"在堆上。
var c = 42.2-->42.2在堆上。
var d = 123n-->123n在堆上。
var e = new Object();--> 对象位于堆上。
同样,在某些特定于引擎的情况下,可以在适当的情况下优化堆分配。例如,V8(受其他一些虚拟机的启发)有一个众所周知的技巧,它可以使用标记位直接在指针中存储小整数(“Smis”),因此在这种情况下,指针实际上并不指向value,可以说指针就是值。另一种技巧称为“NaN-boxing”,它被 Spidermonkey 使用,其效果是所有JS 数字都可以直接存储在指针中(或者技术上相反:在这种方法中,所有内容都是数字,并且存储指针)作为特殊数字)。
另一个例子,一旦函数变得足够热以进行优化,优化编译器可能会发现给定的对象在函数外部不可访问,因此根本不需要分配;如果需要,对象的某些属性将保存在寄存器或堆栈中,以供需要它们的函数部分使用。

所以,总结一下上面的内容:

  • “所有原语都在堆栈上分配”是不正确的。大多数原语都在堆上分配。
  • 有时,引擎可以避免分配(原语和对象),这可能意味着也可能不意味着相应的值会短暂保存在堆栈上(也可以完全消除,或者仅保存在寄存器中)。此类优化永远不会改变可观察到的行为;如果进行优化影响行为,则无法应用优化。
  • 变量,无论它们引用什么,都存储在堆或堆栈上,或者根本不存储,具体取决于情况的要求。