69

目前正在开发一个基于 JavaScript 的动画项目。

我注意到,正确使用setInterval()setTimeout()甚至requestAnimationFrame在没有我的请求的情况下分配内存,并导致频繁的垃圾收集调用。更多 GC 调用 = 闪烁 :-(

例如; 当我通过在 Google Chrome 中调用 init() 来执行以下简单代码时,内存分配 + 垃圾收集在前 20-30 秒内都很好......

function init()
{
    var ref = window.setInterval(function() { draw(); }, 50);
}

function draw()
{
    return true
}

不知何故,在一分钟左右的时间内,分配的内存开始出现奇怪的增加!由于 init() 只被调用一次,分配内存大小增加的原因是什么?

(编辑:chrome截图上传)

铬截图

注意 #1:是的,我尝试在下一个 setInterval() 之前调用 clearInterval()。问题依旧!

注意#2:为了隔离问题,我保持上面的代码简单而愚蠢。

4

8 回答 8

54

编辑:尤里的回答更好。


tl; dr IMO 没有内存泄漏。正斜率只是 setInterval 和 setTimeout 的效果。垃圾被收集,如锯齿模式所见,这意味着根据定义没有内存泄漏。(我认为)。

我不确定有没有办法解决这个所谓的“内存泄漏”。在这种情况下,“内存泄漏”是指对 setInterval 函数的每次调用都会增加内存使用量,如内存分析器中的正斜率所示。

现实情况是没有实际的内存泄漏:垃圾收集器仍然能够收集内存。根据定义,内存泄漏“发生在计算机程序获取内存但未能将其释放回操作系统时”。

如下面的内存配置文件所示,没有发生内存泄漏。每个函数调用都会增加内存使用量。OP 预计,因为这是一遍又一遍地调用同一个函数,所以不应该增加内存。然而,这种情况并非如此。每个函数调用都会消耗内存。最终,垃圾被收集起来,形成锯齿图案。

我已经探索了几种重新排列间隔的方法,它们都导致相同的锯齿模式(尽管一些尝试导致垃圾收集永远不会发生,因为引用被保留)。

function doIt() {
    console.log("hai")
}

function a() {
    doIt();
    setTimeout(b, 50);
}
function b() {
    doIt();
    setTimeout(a, 50);
}

a();

http://fiddle.jshell.net/QNRSK/14/

function b() {
    var a = setInterval(function() {
        console.log("Hello");
        clearInterval(a);
        b();                
    }, 50);
}
b();

http://fiddle.jshell.net/QNRSK/17/

function init()
{
    var ref = window.setInterval(function() { draw(); }, 50);
}
function draw()
{
    console.log('Hello');
}
init();

http://fiddle.jshell.net/QNRSK/20/

function init()
{
    window.ref = window.setInterval(function() { draw(); }, 50);
}
function draw()
{
    console.log('Hello');
    clearInterval(window.ref);
    init();
}
init();​

http://fiddle.jshell.net/QNRSK/21/

显然setTimeout并且setInterval不是 Javascript 的正式部分(因此它们不是 v8 的一部分)。实施留给实施者。我建议你看看node.js 中 setInterval 等的实现

于 2012-12-25T22:14:08.673 回答
31

这里的问题不在于代码本身,它没有泄漏。这是因为时间轴面板的实现方式。当 Timeline 记录事件时,我们会在每次调用 setInterval 回调时收集 JavaScript 堆栈跟踪。堆栈跟踪首先在 JS 堆中分配,然后复制到本机数据结构中,堆栈跟踪复制到本机事件后,它成为 JS 堆中的垃圾。这反映在图表上。禁用以下调用http://trac.webkit.org/browser/trunk/Source/WebCore/inspector/TimelineRecordFactory.cpp#L55使内存图平坦。

有一个与此问题相关的错误:https ://code.google.com/p/chromium/issues/detail?id=120186

于 2013-02-13T10:34:06.073 回答
12

每次进行函数调用时,它都会创建一个堆栈帧。与许多其他语言不同,Javascript 将堆栈帧存储在堆上,就像其他所有语言一样。这意味着每次调用一个函数(每 50 毫秒执行一次)时,都会将一个新的堆栈帧添加到堆中。这加起来并最终被垃圾收集。

考虑到 Javascript 的工作原理,这有点不可避免。唯一可以真正减轻它的方法是使堆栈帧尽可能小,我相信所有的实现都会这样做。

于 2013-02-12T18:11:19.137 回答
8

我想回复您关于 setInterval 和闪烁的评论:

我注意到,正确使用 setInterval()、setTimeout() 甚至 requestAnimationFrame 会在没有我请求的情况下分配内存,并导致频繁的垃圾收集调用。更多 GC 调用 = 闪烁 :-(

您可能想尝试用基于 setTimeout的不那么邪恶的自调用函数替换 setInterval 调用。Paul Irish 在名为“我从 jQuery 源代码中学到的 10 件事”的演讲中提到了这一点(视频在这里,注释在这里见 #2)。您所做的是将您对 setInterval 的调用替换为一个函数,该函数在完成应做的工作后通过 setTimeout 间接调用自身。引用谈话内容:

许多人认为 setInterval 是一个邪恶的函数。无论函数是否完成,它都会以指定的时间间隔继续调用函数。

使用上面的示例代码,您可以从以下位置更新您的 init 函数:

function init() 
{
    var ref = window.setInterval(function() { draw(); }, 50);
}

至:

function init()
{
     //init stuff

     //awesome code
     
     //start rendering
     drawLoop();
}

function drawLoop()
{
   //do work
   draw();

   //queue more work
   setTimeout(drawLoop, 50);
}

这应该会有所帮助,因为:

  1. 在完成之前,您的渲染循环不会再次调用 draw()
  2. 正如上述许多答案所指出的那样,来自 setInterval 的所有不间断函数调用都会给浏览器带来开销。
  3. 调试会更容易一些,因为您不会被 setInterval 的持续触发所打断

希望这可以帮助!

于 2013-02-12T19:40:44.433 回答
3

Chrome 几乎没有看到您的程序有任何内存压力(按照今天的标准,1.23 MB 的内存使用率非常低),因此它可能不认为它需要积极地进行 GC。如果您修改程序以使用更多内存,您将看到垃圾收集器启动。例如试试这个:

<!html>
<html>
<head>
<title>Where goes memory?</title>
</head>
<body>

Greetings!

<script>
function init()
{
    var ref = window.setInterval(function() { draw(); }, 50);
}

function draw()
{
    var ar = new Array();
    for (var i = 0; i < 1e6; ++i) {
        ar.push(Math.rand());
    }
    return true
}

init();
</script>

</body>
</html>

当我运行这个时,我得到了一个锯齿形的内存使用模式,峰值在 13.5MB 左右(同样,按照今天的标准来说相当小)。

PS:我的浏览器的细节:

Google Chrome   23.0.1271.101 (Official Build 172594)
OS  Mac OS X
WebKit  537.11 (@136278)
JavaScript  V8 3.13.7.5
Flash   11.5.31.5
User Agent  Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11
于 2012-12-25T22:44:46.563 回答
3

尝试在没有匿名函数的情况下执行此操作。例如:

function draw()
{
    return true;
}

function init()
{
    var ref = window.setInterval(draw, 50);
}

它的行为仍然相同吗?

于 2013-02-12T16:35:37.187 回答
2

似乎没有内存泄漏。只要 GC 后内存使用量再次下降,并且总体内存使用量没有平均上升趋势,就没有泄漏。

我在这里看到的“真正”问题是setInterval确实使用内存来操作,而且看起来它不应该分配任何东西。实际上它需要分配一些东西:

  1. 它将需要分配一些堆栈空间来执行匿名函数和 draw() 例程。
  2. 我不知道是否需要分配任何临时数据来自己执行调用(可能不需要)
  3. 它需要分配少量存储空间来保存true来自draw().
  4. 在内部,setInterval 可能会分配额外的内存来重新安排重复发生的事件(我不知道它在内部是如何工作的,它可能会重复使用现有的记录)。
  5. JIT 可能会尝试跟踪该方法,这将为跟踪和一些指标分配额外的存储空间。VM 可能会确定此方法太小而无法跟踪它,我不确切知道打开或关闭跟踪的所有阈值是多少。如果您运行此代码的时间足够长,以便 VM 将其识别为“热”,它可能会分配更多内存来保存 JIT 编译的机器代码(之后,我预计平均内存使用量会减少,因为生成的机器代码应该在大多数情况下分配更少的内存)

每次执行匿名函数时,都会分配一些内存。当这些分配加起来达到某个阈值时,GC 将启动并清理以使您回到基本水平。循环将继续如此,直到您将其关闭。这是预期的行为。

于 2013-02-12T16:46:24.390 回答
1

我也有同样的问题。客户向我反映,其计算机的内存每次都在增加。起初我觉得一个网络应用程序可以做到这一点真的很奇怪,即使它是通过一个简单的浏览器访问的。我注意到这只发生在 Chrome 中。

但是,我开始与合作伙伴进行调查,通过 Chrome 的开发人员工具和管理器任务,我们可以看到客户端向我报告的内存增加。

然后我们看到一个 jquery 函数(请求动画帧)被一遍又一遍地加载,增加了系统内存。在那之后,感谢这篇文章,我们看到了一个 jquery 倒计时,因为它有一个“SETINTERVAL”,每次都在更新我的应用程序布局中的日期。

当我使用 ASP.NET MVC 时,我只是从 BundleConfig 和我的布局中退出了这个 jquery 脚本倒计时,用以下代码替换我的时间倒计时:

@(DateTime.Now.ToString("dd/MM/yyyy HH:mm"))
于 2014-11-17T16:45:22.223 回答