JavaScript setInterval()方法会导致内存泄漏吗?

时间:2012-12-25 21:27:11

标签: javascript memory-leaks setinterval

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

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

例如;当我在谷歌浏览器中调用init()执行以下简单代码时,内存分配+垃圾收集在前20-30秒内没问题......

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

function draw()
{
    return true
}

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

(编辑:Chrome截图已上传)

chrome screenshot

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

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

8 个答案:

答案 0 :(得分:51)

编辑:Yury's answer更好。


tl;博士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/

显然setTimeoutsetInterval不是Javascript的正式部分(因此它们不是v8的一部分)。实施由实施者决定。我建议你看看the implementation of setInterval and such in node.js

答案 1 :(得分:27)

这里的问题不在于代码本身,它不会泄漏。这是因为Timeline面板的实现方式。当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

答案 2 :(得分:12)

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

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

答案 3 :(得分:6)

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

  

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

您可能希望尝试使用基于setTimeout的 less evil 自调用函数替换setInterval调用。保罗·爱尔兰在谈话中提到了这一点,我从jQuery来源中了解了10件事(视频here,注释here见#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
  4. 的持续触发打断

    希望这有帮助!

答案 4 :(得分: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

答案 5 :(得分:3)

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

function draw()
{
    return true;
}

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

它的行为方式是否仍然相同?

答案 6 :(得分:2)

似乎没有内存泄漏。只要在GC之后内存使用量再次减少,并且整体内存使用率平均不会上升,就没有泄漏。

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

  1. 需要分配一些堆栈空间来执行匿名函数和draw()例程。
  2. 我不知道是否需要分配任何临时数据来自己执行调用(可能不是)
  3. 需要分配少量存储空间才能保留true的{​​{1}}返回值。
  4. 在内部,setInterval可以分配额外的内存来重新安排重复发生的事件(我不知道它是如何在内部工作的,它可能会重用现有的记录)。
  5. JIT可能会尝试跟踪该方法,该方法将为跟踪和某些指标分配额外的存储空间。 VM可能会确定此方法太小而无法跟踪它,我不确切知道所有阈值是用于打开还是关闭跟踪。如果你运行这段代码足够让VM将其识别为“热门”,它可能会分配更多的内存来保存JIT编译的机器代码(之后,我会期望平均内存使用量减少,因为生成的机器代码应该在大多数情况下分配更少的内存)
  6. 每次执行匿名函数时,都会分配一些内存。当这些分配加起来达到某个阈值时,GC将启动并清理以使您回到基准级别。循环将继续这样,直到你关闭它。这是预期的行为。

答案 7 :(得分:1)

我也有同样的问题。客户报告我,计算机的内存每次都在增加。起初我认为,即使它是由一个简单的浏览器访问,一个Web应用程序可以做到这一点真的很奇怪。我注意到这只发生在Chrome中。

然而,我开始与合作伙伴一起调查并通过Chrome的开发者工具和经理任务,我们可以看到客户报告我的内存增加。

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

当我使用ASP.NET MVC时,我刚从BundleConfig退出此jquery脚本倒计时,并从我的布局中用以下代码替换我的时间倒计时:

@(DateTime.Now.ToString("dd/MM/yyyy HH:mm"))