关于闭包,LexicalEnvironment和GC

ota*_*tay 13 javascript closures garbage-collection

作为ECMAScriptv5,每当控件输入代码时,enginge创建一个LexicalEnvironment(LE)和一个VariableEnvironment(VE),对于功能代码,这两个对象是完全相同的引用,它是调用NewDeclarativeEnvironment(ECMAScript v5 10.4)的结果. 3),在函数代码中声明的所有变量都存储在VariableEnvironment(ECMAScript v5 10.5)的环境记录组件中,这是闭包的基本概念.

令我困惑的是Garbage Collect如何使用这种闭包方法,假设我有以下代码:

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

在该行之后var f2 = f1(),我们的对象图将是:

global -> f2 -> f2's VariableEnvironment -> f1's VariableEnvironment -> o
Run Code Online (Sandbox Code Playgroud)

从我的小知识来看,如果javascript引擎使用引用计数方法进行垃圾收集,则该对象o至少有1次重新引用,并且永远不会被GCed.显然这会导致浪费内存,因为o永远不会被使用但总是存储在内存中.

有人可能会说引擎知道f2的VariableEnvironment不使用f1的VariableEnvironment,所以整个f1的VariableEnvironment都会被GCed,所以还有另一个代码片段可能会导致更复杂的情况:

function f1() {
    var o1 = LargeObject.fromSize('10MB');
    var o2 = LargeObject.fromSize('10MB');
    return function() {
        alert(o1);
    }
}
var f2 = f1();
Run Code Online (Sandbox Code Playgroud)

在这种情况下,f2使用o1存储在f1的VariableEnvironment中的对象,因此f2的VariableEnvironment必须保持对f1的VariableEnvironment的引用,该结果o2也不能被GC,这进一步导致内存浪费.

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

谢谢.

wax*_*ing 9

tl;博士回答: "只有从内部fns引用的变量才是在V8中分配的堆.如果使用eval,那么所有变量都会被引用." .在第二个示例中,o2可以在堆栈上分配,并在f1退出后丢弃.


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

function outer(node) {
    node.onclick = function inner() { 
        // some code not referencing "node"
    };
}
Run Code Online (Sandbox Code Playgroud)

inner封闭的情况下node,形成一个循环引用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");
Run Code Online (Sandbox Code Playgroud)

请参阅使用此jsfiddle示例.没有引用任何一个xy内部inner,但它们仍然可以使用eval.(令人惊讶的是,如果您使用别名eval,比如说myeval,并且调用myeval,则不会获得新的执行上下文 - 这甚至在规范中,请参阅ECMA-262中的10.4.2和15.1.2.1.1节.)


编辑:根据你的评论,似乎一些现代引擎实际上做了一些聪明的技巧,所以我试图挖一点.我遇到了讨论这个问题的论坛帖子,特别是关于如何在V8中分配变量的推文的链接.它还专门触及了这个eval问题.似乎它必须在所有内部函数中解析代码.并查看引用了哪些变量,或者是否eval使用了变量,然后确定是应在堆上还是在堆栈上分配每个变量.很简约.这是另一个博客,其中包含有关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!");
    };
}
Run Code Online (Sandbox Code Playgroud)

现在,init已完成调用,someLargeVariable不再被引用,应该有资格删除,但我怀疑它不是,除非内部函数drawSomeWidget已被优化(内联?).如果是这样,当使用自执行函数模仿具有私有/公共方法的类时,这可能会经常发生.


回答下面的Raynos评论.我在调试器中尝试了上面的场景(稍加修改),结果就像我预测的那样,至少在Chrome中是这样的:

Chrome调试器的屏幕截图 在执行内部函数时,someLargeVariable仍然在范围内.

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

Chrome调试器2的屏幕截图 现在someLargeVariable不在范围内,因为它可以在堆栈上分配.