ChatGPT解决这个技术问题 Extra ChatGPT

JavaScript 闭包是如何被垃圾回收的

我记录了以下 Chrome bug,这导致我的代码中有许多严重且不明显的内存泄漏:

(这些结果使用 Chrome 开发工具的 memory profiler,它运行 GC,然后对所有没有被垃圾收集的东西进行堆快照。)

在下面的代码中,someClass 实例被垃圾回收(良好):

var someClass = function() {};

function f() {
  var some = new someClass();
  return function() {};
}

window.f_ = f();

但在这种情况下不会被垃圾收集(坏):

var someClass = function() {};

function f() {
  var some = new someClass();
  function unreachable() { some; }
  return function() {};
}

window.f_ = f();

以及相应的截图:

https://i.stack.imgur.com/ZSVf0.png

如果对象被同一上下文中的任何其他闭包引用,则闭包(在本例中为 function() {})似乎使所有对象保持“活动”,无论该闭包本身是否可访问。

我的问题是关于其他浏览器(IE 9+ 和 Firefox)中闭包的垃圾收集。我对 webkit 的工具非常熟悉,比如 JavaScript 堆分析器,但是我对其他浏览器的工具知之甚少,所以我无法对此进行测试。

在这三种情况中,IE9+ 和 Firefox 会垃圾收集 someClass 实例中的哪一种?

对于初学者,Chrome 如何让您测试哪些变量/对象被垃圾回收,以及何时发生?
也许控制台正在保留对它的引用。当你清除控制台时它会被 GCed 吗?
@david 在最后一个示例中, unreachable 函数从未执行,因此实际上没有记录任何内容。
即使我们似乎面对事实,我也很难相信如此重要的错误已经发生了。但是我一次又一次地查看代码,我没有找到任何其他合理的解释。你试图完全不在控制台中运行代码(也就是让浏览器从加载的脚本中自然地运行它)?
@some,我以前读过那篇文章。它的副标题是“处理 JavaScript 应用程序中的循环引用”,但 JS/DOM 循环引用的问题不适用于现代浏览器。它提到了闭包,但在所有的例子中,有问题的变量仍然可能被程序使用。

J
Jeremy

据我所知,这不是错误,而是预期的行为。

来自 Mozilla 的 Memory management page:“截至 2012 年,所有现代浏览器都配备了标记和清除垃圾收集器。” “限制:对象需要明确地不可访问

在您失败的示例中,some 在闭包中仍然可以访问。我尝试了两种方法使其无法访问并且都可以工作。当您不再需要它时设置 some=null,或者设置 window.f_ = null; 它将消失。

更新

我已经在 Windows 上的 Chrome 30、FF25、Opera 12 和 IE10 中尝试过。

standard 没有说明垃圾收集,但提供了一些关于应该发生什么的线索。

第 13 节函数定义,第 4 步:“让闭包成为创建 13.2 中指定的新函数对象的结果”

第 13.2 节“范围指定的词法环境”(范围 = 闭包)

第 10.2 节词法环境:

“(内部)词汇环境的外部引用是对在逻辑上围绕内部词汇环境的词汇环境的引用。外部词汇环境当然可以有自己的外部词汇环境。词汇环境可以作为外部词汇环境多个内部词法环境的环境。例如,如果函数声明包含两个嵌套函数声明,则每个嵌套函数的词法环境将具有当前执行周围函数的词法环境作为它们的外部词法环境。

因此,一个函数将可以访问父级的环境。

因此,some 应该在返回函数的闭包中可用。

那为什么总是不可用呢?

在某些情况下,Chrome 和 FF 似乎足够聪明,可以消除该变量,但在 Opera 和 IE 中,some 变量在闭包中都可用(注意:要查看此设置,请在 return null 上设置断点并检查调试器)。

GC可以改进以检测函数中是否使用了some,但这会很复杂。

一个不好的例子:

var someClass = function() {};

function f() {
  var some = new someClass();
  return function(code) {
    console.log(eval(code));
  };
}

window.f_ = f();
window.f_('some');

在上面的示例中,GC 无法知道变量是否被使用(代码经过测试并在 Chrome30、FF25、Opera 12 和 IE10 中工作)。

如果通过将另一个值分配给 window.f_ 来破坏对对象的引用,则会释放内存。

在我看来,这不是一个错误。


但是,一旦 setTimeout() 回调运行,setTimeout() 回调的函数范围就完成了,整个范围应该被垃圾回收,释放它对 some 的引用。不再有任何可以运行的代码可以到达闭包中的 some 实例。它应该被垃圾收集。最后一个例子更糟糕,因为 unreachable() 甚至没有被调用并且没有人引用它。它的范围也应该是 GCed。这两个似乎都是错误。 JS 中没有语言要求来“释放”函数范围内的东西。
@some 它不应该。函数不应该关闭它们没有在内部使用的变量。
它可以被空函数访问,但它不是,所以没有对它的实际引用,所以应该很清楚。垃圾收集跟踪实际引用。它不应该保留所有可能被引用的东西,只保留实际引用的东西。一旦最后一个 f() 被调用,就不再有对 some 的实际引用。它是不可访问的,应该被 GCed。
@jfriend00 我在(标准)[ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf] 中找不到任何关于只有它在内部使用的变量应该可用的内容。在第 13 节,生产步骤 4:让闭包成为创建 13.2 中指定的新 Function 对象的结果,10.2“外部环境引用用于建模词法环境值的逻辑嵌套。 (内部)词汇环境的外部引用是对逻辑上围绕内部词汇环境的词汇环境的引用。”
好吧,eval 确实是特殊情况。例如,eval 不能是别名 (developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…),例如 var eval2 = eval。如果使用了 eval(并且因为它不能用不同的名称调用,这很容易做到),那么我们必须假设它可以使用范围内的任何东西。
P
Paul Draper

我在 IE9+ 和 Firefox 中对此进行了测试。

function f() {
  var some = [];
  while(some.length < 1e6) {
    some.push(some.length);
  }
  function g() { some; } //removing this fixes a massive memory leak
  return function() {};   //or removing this
}

var a = [];
var interval = setInterval(function() {
  var len = a.push(f());
  if(len >= 500) {
    clearInterval(interval);
  }
}, 10);

实时网站 here

我希望使用最少的内存得到一个包含 500 个 function() {} 的数组。

不幸的是,事实并非如此。每个空函数都持有一个(永远无法访问,但不是 GC'ed)一百万个数字的数组。

Chrome 最终停止并死掉,Firefox 在使用了近 4GB 的 RAM 后完成了整个事情,而 IE 逐渐变慢,直到它显示“内存不足”。

删除任一注释行可以解决所有问题。

似乎所有这三种浏览器(Chrome、Firefox 和 IE)都为每个上下文而不是每个闭包保留了环境记录。 Boris 假设这个决定背后的原因是性能,这似乎很可能,尽管我不确定根据上述实验可以调用它的性能如何。

如果需要引用 some 的闭包(当然我在这里没有使用它,但想象一下我用过),如果不是

function g() { some; }

我用

var g = (function(some) { return function() { some; }; )(some);

它将通过将闭包移动到与我的其他函数不同的上下文来解决内存问题。

这会让我的生活更加乏味。

PS出于好奇,我在Java中尝试了这个(利用它在函数内部定义类的能力)。 GC 就像我最初希望的 Javascript 一样工作。


我认为外部函数 var g = (function(some) { return function() { some; }; } )(some); 缺少右括号
我想知道最新的 JS 引擎是否仍然如此?
B
Boris Zbarsky

启发式方法各不相同,但实现此类事情的常用方法是为您的情况下对 f() 的每次调用创建一个环境记录,并仅存储实际关闭的 f 的本地变量(由 some 关闭)在该环境记录中。然后在对 f 的调用中创建的任何闭包都会使环境记录保持活动状态。我相信这至少是 Firefox 实现闭包的方式。

这具有快速访问封闭变量和实现简单的好处。它具有观察到的效果的缺点,即关闭某个变量的短期闭包会导致它通过长期闭包保持活动状态。

可以尝试为不同的闭包创建多个环境记录,具体取决于它们实际关闭的内容,但这会很快变得非常复杂,并可能导致其自身的性能和内存问题......


感谢您的见解。我得出结论,这也是 Chrome 实现闭包的方式。我一直认为它们是以后一种方式实现的,其中每个闭包只保留它需要的环境,但事实并非如此。我想知道创建多个环境记录是否真的那么复杂。与其聚合闭包的引用,不如将每个闭包视为唯一的闭包。我猜想性能考虑是这里的原因,尽管对我来说拥有共享环境记录的后果似乎更糟。
在某些情况下,后一种方式会导致需要创建的环境记录数量激增。除非您尽可能地尝试跨功能共享它们,否则您需要一堆复杂的机器来做到这一点。这是可能的,但有人告诉我,性能权衡有利于当前的方法。
记录数等于创建的闭包数。我可以将 O(n^2)O(2^n) 描述为爆炸,但不是成比例的增加。
好吧,与 O(1) 相比,O(N) 是一个爆炸式增长,尤其是当每个人都可以占用相当多的内存时……再说一次,我不是这方面的专家;在 irc.mozilla.org 上的 #jsapi 频道上询问可能会比我提供的权衡取舍得到更好和更详细的解释。
@Esailija 不幸的是,这实际上很常见。您所需要的只是函数中的一个大临时变量(通常是一个大型类型数组),一些随机的短期回调使用和一个长期闭包。对于编写网络应用程序的人来说,最近出现了很多次......
A
Avinash Maurya

在函数调用之间保持状态假设您有函数 add(),并且您希望它将在多次调用中传递给它的所有值相加并返回总和。

像添加(5); // 返回 5

添加(20); // 返回 25 (5+20)

添加(3); // 返回 28 (25 + 3)

两种方法你可以做到这一点首先是正常定义一个全局变量当然,你可以使用一个全局变量来保存总数。但是请记住,如果您(ab)使用全局变量,这个家伙会活活吃掉您。

现在使用闭包的最新方法没有定义全局变量

(function(){ var addFn = function addFn(){ var total = 0; return function(val){ total += val; return total; } }; var add = addFn(); console.log(add(5) ); console.log(add(20)); console.log(add(3)); }());


A
Avinash Maurya

function Country(){ console.log("确保国家电话");返回函数状态(){ var totalstate = 0; if(totalstate==0){ console.log("makesure statecall");返回函数(val){总状态+= val; console.log("你好:"+totalstate);返回总状态; } }else{ console.log("hey:"+totalstate); } }; }; var CA=国家();变种 ST=CA(); ST(5); //我们添加了 5 个状态 ST(6); //几年后,我们需要添加新的 6 个状态,所以现在总共 11 个 ST(4); // 15 var CB=Country(); var STB=CB();机顶盒(5); //5 机顶盒(8); //13 机顶盒(3); //16 var CX=国家; var d=国家();控制台.log(CX); //在CA console.log(d)中存储为国家的副本; //在国家函数中作为返回值存储在d中


请描述答案
A
Avinash Maurya

(function(){ function addFn(){ var total = 0; if(total==0){ return function(val){ total += val; console.log("hello:"+total); return total+9 ; } }else{ console.log("hey:"+total); } }; var add = addFn(); console.log(add); var r= add(5); //5 console.log(" r:"+r); //14 var r= add(20); //25 console.log("r:"+r); //34 var r= add(10); //35 console.log ("r:"+r); //44 var addB = addFn(); var r= addB(6); //6 var r= addB(4); //10 var r= addB(19); / /29 }());