作为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引擎处理这样的确切步骤是什么执行垃圾收集时的对象图。
感谢。
答案 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
中没有对y
或inner
的引用,但仍然可以使用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中是这样的:
当执行内部函数时,someLargeVariable仍在范围内。
如果我在内部someLargeVariable
方法中注释了对drawSomeWidget
的引用,那么您会得到不同的结果:
现在someLargeVariable
不在范围内,因为它可以在堆栈上分配。
答案 1 :(得分:1)
GC没有标准的实施规范,每个引擎都有自己的实现。我知道v8的一个小概念,它有一个非常令人印象深刻的垃圾收集器(世界停止,世代,准确)。如上面的示例2所示,v8引擎具有以下步骤:
在解析函数文字上,它创建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"很有趣。