关于闭包,LexicalEnvironment和GC

时间:2011-12-29 09:02:32

标签: javascript garbage-collection closures

作为ECMAScriptv5,每当控件输入代码时,enginge为功能代码<创建 LexicalEnvironment (LE)和 VariableEnvironment (VE) / strong>,这两个对象与调用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();
在行var f2 = f1()之后

,我们的对象图将是:

global -> f2 -> f2's VariableEnvironment -> f1's VariableEnvironment -> o

从我的小知识来看,如果javascript引擎使用引用计数方法进行垃圾回收,则对象o至少具有 1 refenrence ,永远不会被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无法进行GC操作,这进一步导致内存浪费。

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

感谢。

3 个答案:

答案 0 :(得分:8)

tl;博士回答: "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"
    };
}

其中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");

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


编辑:根据你的评论,似乎一些现代引擎实际上做了一些聪明的技巧,所以我试图挖掘更多。我遇到了forum thread讨论这个问题,特别是a tweet about how variables are allocated in V8的链接。它还特别涉及eval问题。似乎它必须在所有内部函数中解析代码。并查看引用了哪些变量,或者是否使用了eval,然后确定是应在堆上还是在堆栈上分配每个变量。很简约。这里是another blog,其中包含有关ECMAScript实现的大量详细信息。

这暗示即使内部函数永远不会“逃避”调用,它仍然可以强制在堆上分配变量。 E.g:

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已经有被优化了(内联?)。如果是这样,当使用自执行函数模仿具有私有/公共方法的类时,这可能会经常发生。


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

Screenshot of Chrome debugger 当执行内部函数时,someLargeVariable仍在范围内。

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

Screenshot of Chrome debugger 2 现在someLargeVariable不在范围内,因为它可以在堆栈上分配。

答案 1 :(得分:1)

GC没有标准的实施规范,每个引擎都有自己的实现。我知道v8的一个小概念,它有一个非常令人印象深刻的垃圾收集器(世界停止,世代,准确)。如上面的示例2所示,v8引擎具有以下步骤:

  1. 创建f1的名为f1的VariableEnvironment对象。
  2. 创建该对象后,V8创建一个名为H1的初始隐藏类f1。
  3. 表示f1的点是根级别的f2。
  4. 根据H1创建另一个隐藏类H2,然后向H2添加信息,将对象描述为具有一个属性o1,将其存储在f1对象中的偏移0处。
  5. 更新f1指向H2,表示f1应使用H2代替H1。
  6. 根据H2创建另一个隐藏类H3,并添加属性o2,将其存储在f1对象中的偏移量1处。
  7. 更新f1指向H3。
  8. 创建名为a1的匿名VariableEnvironment对象。
  9. 创建一个名为A1的初始隐藏类a1。
  10. 表示a1父级是f1。
  11. 在解析函数文字上,它创建FunctionBody。仅在调用函数时解析FunctionBody。以下代码表示在解析器时间

    时不会抛出错误
    function p(){
      return function(){alert(a)}
    }
    p();
    

    所以在GC时间H1,H2会被扫描,因为没有参考点。在我看来如果代码是懒惰的编译,没有办法表明在a1中声明的o1变量是对f1的引用,它使用JIT。

答案 2 :(得分:0)

  

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

大多数javascript引擎使用compacting mark and sweep垃圾收集器的一些变体,而不是简单的引用计数GC,因此引用循环不会导致问题。

他们也倾向于做一些技巧,以便涉及DOM节点的循环(由JavaScript堆外部的浏览器计算引用)不会引入无法收集的循环。 The XPCOM cycle collector为Firefox执行此操作。

  

循环收集器花费大部分时间累积(并忘记)可能涉及垃圾循环的XPCOM对象的指针。这是收集器操作的空闲阶段,其中nsAutoRefCnt的特殊变体在收集器中非常快速地注册和注销它们,因为它们通过“可疑”引用计数事件(从N + 1到N,非零) N)。

     

收集器会定期唤醒并检查已暂停在其缓冲区中的所有可疑指针。这是收集器操作的扫描阶段。在这个阶段,收集器会反复询问每个候选者是否有单个循环集合帮助程序类,如果该帮助程序存在,则收集器会要求帮助程序描述候选者(拥有)子项。通过这种方式,收集器可以构建可疑对象可以访问的所有权子图的图片。

     

如果收集器找到一组对象全部引用彼此,并确定对象的引用计数全部由组内的内部指针计算,则它会认为该组循环垃圾,然后它会尝试自由。这是收藏家操作的无关联阶段。在这个阶段,收集器遍历它找到的垃圾对象,再次咨询他们的帮助对象,要求帮助对象将每个对象与其直接子对象“取消链接”。

     

请注意,收集器也知道如何遍历JS堆,并且可以找到传入和传出它的所有权周期。

EcmaScript和谐可能还包括ephemerons以提供弱持有的引用。

您可能会发现"The future of XPCOM memory management"很有趣。