与ECMAScriptv5一样,每次控件输入代码时,引擎都会创建 LexicalEnvironment (LE)和 VariableEnvironment (VE),对于功能代码,这两个对象与调用结果完全相同NewDeclarativeEnvironment(ECMAScript v5 10.4.3)和所有在功能代码中声明的变量都存储在环境记录组件变量环境(ECMAScript v5 10.5)的环境记录中,这是闭合ojit_r的基本概念。

让我感到困惑的是 Garbage Collect 如何与这种关闭方法一起工作,假设我有如下代码:

function f1() {
    var o = LargeObject.fromSize('10MB');
    return function() {
        // here never uses o
        return 'Hello world';
    }
}
var f2 = f1();

var f2 = f1()行之后,我们的对象图将是:
global -> f2 -> f2's VariableEnvironment -> f1's VariableEnvironment -> o

因此,据我所知,如果javascript引擎使用引用计数方法进行垃圾收集,则对象o至少具有 1防御,并且永远不会被GC化。显然,这将导致内存浪费,因为o将永远不会使用,而是始终存储在内存中。

有人可能会说引擎知道 f2的VariableEnvironment 不使用 f1的VariableEnvironment ,因此整个 f1的VariableEnvironment 都会被GC化,因此还有另一个代码段可能会导致更复杂:
function f1() {
    var o1 = LargeObject.fromSize('10MB');
    var o2 = LargeObject.fromSize('10MB');
    return function() {
        alert(o1);
    }
}
var f2 = f1();

在这种情况下,f2使用o1对象,该对象存储在 f1的VariableEnvironment 中,因此 f2的VariableEnvironment 必须保留对 f1的VariableEnvironment 的引用,这将导致o2浪费在内存中。

所以我会问,现代的javascript引擎(JScript.dll / V8 / SpiderMonkey ...)如何处理这种情况,是否有标准的指定规则或基于实现的?javascript引擎在处理此类对象图时的确切步骤是什么?执行垃圾回收。

谢谢。

最佳答案

tl; dr答案: "Only variables referenced from inner fns are heap allocated in V8. If you use eval then all vars assumed referenced."。在第二个示例中,可以将o2分配在堆栈上,并在f1退出后将其丢弃。

我认为他们无法应付。至少我们知道某些引擎不能,因为众所周知,这是导致许多内存泄漏的原因,例如:

function outer(node) {
    node.onclick = function inner() {
        // some code not referencing "node"
    };
}

其中innernode上关闭,形成一个循环引用inner -> outer's VariableContext -> node -> inner,即使从文档中删除了DOM节点,也永远不会在IE6中释放它。不过,某些浏览器可以很好地处理此问题:循环引用本身不是问题,而IE6中的GC实现才是问题。但是现在我偏离主题了。

打破循环引用的一种常见方法是在outer的末尾消除所有不必要的变量。即,设置node = null。然后的问题是,现代的javascript引擎是否可以为您执行此操作,它们可以以某种方式推断出inner中未使用变量吗?

我认为答案是否定的,但可以证明我是错误的。原因是以下代码执行得很好:
function get_inner_function() {
    var x = "very big object";
    var y = "another big object";
    return function inner(varName) {
        alert(eval(varName));
    };
}

func = get_inner_function();

func("x");
func("y");

使用this jsfiddle example自己看看。 x中没有对yinner的引用,但仍可以使用eval访问它们。 (令人惊讶的是,如果您将eval别名为别的东西,例如myeval,并调用myeval,则不会获得新的执行上下文-甚至在规范中,请参阅ECMA-262中的10.4.2和15.1.2.1.1节。 )

编辑:根据您的评论,看来有些现代引擎实际上在做一些巧妙的窍门,所以我尝试多挖一些。我碰到了这个forum thread,讨论了这个问题,尤其是a tweet about how variables are allocated in V8的链接。它还专门涉及eval问题。似乎它必须解析所有内部函数中的代码。并查看引用了哪些变量,或者是否使用了eval,然后确定是应在堆上还是在堆栈上分配每个变量。挺整洁的。这是another blog,其中包含有关ECMAScript实现的许多详细信息。

这意味着即使内部函数从不“转义”调用,它仍然可以强制在堆上分配变量。例如。:
function init(node) {

    var someLargeVariable = "...";

    function drawSomeWidget(x, y) {
        library.draw(x, y, someLargeVariable);
    }

    drawSomeWidget(1, 1);
    drawSomeWidget(101, 1);

    return function () {
        alert("hi!");
    };
}

现在,由于init已完成其调用,因此不再引用someLargeVariable,应该可以删除它,但是我怀疑它不是,除非内部函数drawSomeWidget已被优化(内联?)。如果是这样,当使用自执行函数通过私有(private)/公共(public)方法模仿类时,这种情况很可能经常发生。

回答雷诺斯下面的评论。我在调试器中尝试了上述情况(稍作修改),结果至少在Chrome中符合我的预期:

当执行内部函数时,someLargeVariable仍在范围内。

如果我在内部someLargeVariable方法中注释掉对drawSomeWidget的引用,那么您将得到不同的结果:

现在someLargeVariable不在范围内,因为可以在堆栈上分配它。

09-10 12:33