Javascript 关于闭包、词汇环境和GC

Javascript 关于闭包、词汇环境和GC,javascript,garbage-collection,closures,Javascript,Garbage Collection,Closures,作为ECMAScriptv5,每次控件输入代码时,enginge都会创建一个词汇环境(LE)和一个可变环境(VE),对于函数代码,这两个对象是完全相同的引用,这是调用NewDeclarativeEnvironment()的结果,而函数代码中声明的所有变量都存储在VariableEnvironment()的环境记录中,这是关闭的基本概念 让我困惑的是垃圾收集如何使用这种闭包方法,假设我有如下代码: function f1() { var o = LargeObject.fromSize('

作为ECMAScriptv5,每次控件输入代码时,enginge都会创建一个词汇环境(LE)和一个可变环境(VE),对于函数代码,这两个对象是完全相同的引用,这是调用NewDeclarativeEnvironment()的结果,而函数代码中声明的所有变量都存储在VariableEnvironment()的环境记录中,这是关闭的基本概念

让我困惑的是垃圾收集如何使用这种闭包方法,假设我有如下代码:

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个引用,并且永远不会被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();
在这种情况下,
f2
使用存储在f1的VariableEnvironment中的
o1
对象,因此f2的VariableEnvironment必须保留对f1的VariableEnvironment的引用,这导致
o2
也无法进行GCed,这进一步导致内存浪费

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


谢谢。

tl;博士回答:。在第二个示例中,
o2
可以在堆栈上分配,并在退出
f1
后丢弃


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

function outer(node) {
    node.onclick = function inner() { 
        // some code not referencing "node"
    };
}
其中,
internal
关闭
node
,形成循环引用
internal->outer的VariableContext->node->internal
,在例如IE6中,即使从文档中删除DOM节点,也不会释放该循环引用。不过有些浏览器处理得很好:循环引用本身不是问题,问题在于IE6中的GC实现。但现在我离题了

打破循环引用的一种常见方法是在
outer
末尾将所有不必要的变量置零。即,设置
节点=null
。现在的问题是,现代javascript引擎是否可以为您做到这一点,它们是否可以推断
内部
中没有使用变量

我想答案是否定的,但我可以证明我错了。原因是以下代码执行得很好:

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");
你自己看看吧。在
内部
中没有对
x
y
的引用,但仍然可以使用
eval
访问它们。(令人惊讶的是,如果您将
eval
别名为其他内容,比如说
myeval
,并调用
myeval
,您不会得到新的执行上下文-这甚至在规范中,请参见ECMA-262中的第10.4.2节和第15.1.2.1.1节。)


编辑:根据你的评论,似乎有些现代引擎确实有一些聪明的技巧,所以我试着挖掘更多。在讨论这个问题时,我遇到了这个问题,特别是一个链接。它还特别涉及
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!");
    };
}
现在,由于
init
已完成其调用,
someLargeVariable
不再被引用,应该可以删除,但我怀疑它没有被引用,除非内部函数
drawSomeWidget
已被优化(内联?)。如果是这样,那么在使用自执行函数模拟具有私有/公共方法的类时,这种情况可能会非常频繁


下面是对雷诺评论的答复。我在调试器中尝试了上述场景(稍加修改),结果正如我预测的,至少在Chrome中是这样的:

当执行内部函数时,someLargeVariable仍在作用域中

如果我注释掉内部
drawSomeWidget
方法中对
someLargeVariable
的引用,则会得到不同的结果:

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

如果javascript引擎使用引用计数方法

大多数javascript引擎使用垃圾收集器的一些变体,而不是简单的引用计数GC,因此引用周期不会导致问题

他们还倾向于做一些技巧,以使涉及DOM节点的循环(由浏览器在JavaScript堆外计数的引用)不会引入无法收集的循环。这是为Firefox做的

循环收集器将大部分时间用于积累(并忘记)指向垃圾循环中可能涉及的XPCOM对象的指针。这是收集器操作的空闲阶段,其中
function p(){
  return function(){alert(a)}
}
p();