与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"
};
}
其中
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
中没有对y
或inner
的引用,但仍可以使用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
不在范围内,因为可以在堆栈上分配它。