Chrome/V8不能垃圾回收循环引用?

15

看一下这个Chrome堆快照的一部分:

Chrome circular reference

它显示了堆中一个对象的保留者,据我所知和所见,该对象应该是垃圾,但尽管如此仍未被收集。

最短路径到根节点毕竟是一个循环路径(实际上从来没有达到根节点)。这让人想知道,快照查看器是如何能够将距离指定为12的?这只是在放弃之前通过循环走过的步数吗?请注意,距离从未低于11。

我读到过清理带有循环引用的子图可能需要几次迭代。但是反复强制收集(使用时间轴选项卡上的垃圾桶按钮)未能清除这些对象。

请注意,浏览 '185' 引用最终会导致相同的system / Context @862399,因此在这里真的没有从根到该对象的路径(至少在此处不可见)。

我是疯了吗,还是垃圾收集器真的坏了?我不记得过去有这个问题。 我使用的是Chrome 45.0.2454.101。 Beta 46.0.2490.64行为相同。


这些方法能被垃圾回收吗?你是否创建了闭包?你可能会无意中创建内存泄漏吗? - Jesse
Google JavaScript风格指南提供了一个闭包的例子,该闭包创建了循环引用,导致内存泄漏。如果这是问题的根源,那么这并不新鲜:1, 2, 3。不确定为什么,但现代的JS引擎似乎没有解决这个非常普遍的内存泄漏来源的方法。 - GOTO 0
@GOTO0 我知道闭包会保留它使用的封闭变量的引用(否则它怎么能工作呢?),但它也保留未使用的封闭变量对我来说是新的,实际上令人失望。然而,这个风格指南说“循环引用因此导致内存泄漏”。这不是一个过时的声明,只适用于引用计数垃圾回收吗?我认为标记和清除应该使这成为一个非问题?如果这一部分是错误的,那么该指南的其余部分是否可信? - Bart van Heukelom
@GOTO0 我做了一个小测试:http://pastebin.com/fPxNxqSy。尽管`x`返回的闭包被`window`保留,但在那里创建的`MyClass`实例`m`没有被保留(在堆快照中可以看到)。`y`中的实例被保留,因为它由于`eval`不能被优化掉。(这是V8的一个实现细节,虽然我怀疑其他现代浏览器的行为相同,但我没有测试过。) - Bart van Heukelom
那个Google风格的代码链接只是某人的建议,尽管是来自Google的某个人,所以像任何建议文章一样,您应该将其视为这样。有一些知名的旧文章涉及此问题,具有基于证据的令人惊叹的建议和示例。然而,您能找到的最好的文档将是您的Chrome配置文件和内存使用情况。使用您的应用程序一段时间,然后将其保持打开状态一段时间而不触摸它,将帮助您识别您自己的内存管理机会,而不是依赖于文章或我的/任何答案。 - Jesse
显示剩余3条评论
1个回答

2

说实话,我们需要快速查看你复制的一些测试代码,但我大致知道你正在经历什么。如果我错了并且你可以提供证明此事的测试代码,请告诉我。

正如你已经知道的那样,“清理” Javascript 希望不再有对要释放的项目的引用。

一个简单的例子:

// Currently not eligible for garbage
var myObj = {
    data : 'test'
};

// Reference data inside myObj
// A unique identifier to myObj.data now exists 
var myData = myObj.data;

// Whoops
myObj = 'something else';

// Because myData exists, so does data and can not be freed either, understandable
// This sounds simple and logical but most do not understand what is happening in the engine
// A relationship has been born between 'myData' and 'data' and as long as it exists, it is not eligible for garbage and remains in memory
console.log( myData );

您的代码可能比这更加复杂,但是这可以帮助解释一下,在某个地方,垃圾可能无法被收集,因为作用域链可以被跟踪到一个引用。

考虑以下内容:

function oops(text){

    function doStuff(){
        return text.toLowerCase();
    }
    return doStuff();
}

// 'doStuff' stays alive thanks to 'test' below.
var test = oops('closure');
< p > 函数doStuff不会被垃圾回收,因为它被test引用。 < /p >
// You can see where this is headed. We have 2 references to the same 'doStuff' function object with separate unique identifiers now below.
var test2 = oops('closures...');

// This is now another unique identifier to 'doStuff'
var test3 = test2;

// doStuff survives yet still
test = 0;
test2 = 0;

// Now we let the function object of 'doStuff' be freed because there is no longer any references to it
test3 = 0;

这实际上是我们创建的内存泄漏。每次调用oops时,您都会创建一个带有唯一标识符的函数对象doStuff

避免这种情况的方法可能是

function doStuff( text ){
    return text.toLowerCase();
}

function oops( text ){
    return doStuff();
}

var test = oops( 'closure' );

现在我们没有内存泄漏了。doStuff被调用而不是被创建。

仔细查看你的代码,你可能会发现你在某个地方做了这件事。

如果你正在处理元素,我认为你可能正在这样做,IBM有一篇关于循环引用的好文章,你可能想看一下。


现在已经很晚了,其中一些内容未经测试,但是理论依然存在,如果我拼错了什么,请让我知道,明天我可以查看,以便日后来访者使用。


谢谢您的回答,但正如上面所评论的,我已经意识到闭包的保留能力,尽管显然不是完全了解。但是,如果我的堆快照中的值可以从GC根访问到,为什么没有显示出从1开始递减的完整路径?这是否意味着不能信任快照查看器?正如问题中所说,我过去没有遇到过这个问题(尽管很难说是应用程序还是Chrome的更改导致了这种情况)。 - Bart van Heukelom
我已经尝试在示例代码中重现这个问题,但没有成功。应该说,它发生的真实应用程序相当大,是一个相当庞大的单页游戏。在泄漏之前,它的内存使用量可以达到几百兆。我不知道浏览器是否已经完全准备好处理这种情况? - Bart van Heukelom
当然,如果内存使用是合理的,浏览器可以处理几百兆字节,但内存越多,延迟就越高。如果我可以提个建议,拍摄一个快照,使用您的应用程序一段时间后再拍摄另一个并进行比较。对于dom元素,您可能会发现需要优化的地方,就像这篇文章中的人所做的那样:https://blog.newrelic.com/2012/07/17/using-chrome-developer-tools-to-find-memory-leaks/ - Jesse
由于每次调用外部函数时都会创建函数实例,因此这是不好的。 - Jesse
1
也许Jesse不小心写成了"return doStuff()"。我认为他的意思是要返回"return doStuff"(不带doStuff调用)。 - NicBright
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接