JavaScript的setInterval()方法是否会导致内存泄漏?

72

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

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

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

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

function draw()
{
    return true
}

不知何故,大约在一分钟内,分配的内存开始怪异地增加!由于 init() 只被调用了一次,这种增加内存大小的原因是什么?

(编辑:上传了 Chrome 截图)

chrome 截图

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

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


4
你如何在Chrome中检查"内存分配+垃圾回收"? - chovy
1
@chovy 设置->工具->任务管理器,也许?但是那不会显示垃圾回收。 - itdoesntwork
3
开发者工具 > 时间轴 > 内存 > 记录 - matahari
我很好奇,如果你只是执行 setInterval(draw, 50);,是否会出现问题?也许与紧密的间隔和匿名函数的作用域建立/拆除有关?虽然我认为Chrome会缓存该函数。 - Erik Reppen
2
已在 Chrome 23 和 26 上确认。 - goat
显示剩余13条评论
9个回答

55

编辑:更好的答案在Yury's answer中。


简而言之,我认为这里没有内存泄漏。正向斜率仅是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 的一部分)。实现留给实施者。建议查看Node.js 中 setInterval 等的实现


5
是的,正如我在原始留言中所发布的那样,我已经知道垃圾回收。它是有效的。我无法理解的是,为什么一个单一的计时器方法,比如setInterval(),会不断消耗内存?从您的JSFiddle中,我可以看到从1.1分钟到4.8分钟内,分配的内存量不断上升(不断增加)!它请求的内存越多=触发的GC调用越多!为了停止不必要的GC调用,需要“驯服” setInterval(),使其停止分配内存... - matahari
1
@user1928710 对不起,我现在正在查看正确的问题。 我猜测函数调用会不断地被推入堆栈中。 - Luqmaan
2
我怀疑这不是堆栈的问题,因为你在这里没有处理递归,而是异步操作。 - bfavaretto
1
@inspectahdeck 我认为你是对的,这只是在给定时间间隔内每个函数调用所需的常规内存分配。 - bfavaretto
1
希望我们能从Chromium论坛(http://code.google.com/p/chromium/issues/detail?id=167647)得到一个官方答复,并了解是否只是定时器方法的(非)常规内存分配请求... - matahari
显示剩余8条评论

34

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

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


13
每次调用函数时,都会创建一个stack frame。与许多其他语言不同,Javascript将堆栈帧存储在堆上,就像其他所有东西一样。这意味着每次调用函数(您每50ms执行一次),都会向堆添加一个新的堆栈帧。这些累加起来最终会被垃圾回收。
鉴于Javascript的工作方式,这似乎是不可避免的。唯一真正可以做的事情是使堆栈帧尽可能小,我相信所有实现都会这样做。

10

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

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

您可能希望尝试用一个基于 setTimeout 的“不那么邪恶”的自调用函数来替换 setInterval 调用。Paul Irish 在名为《从 jQuery 源码中学到的 10 件事》的演讲中提到了这一点(视频在这里,笔记在这里,看第二项)。您需要将 setInterval 调用替换为一个函数,在完成应该完成的工作后通过 setTimeout 间接地调用自身。引用该演讲中的话:

许多人认为 setInterval 是一个邪恶的函数。它会按照指定的时间间隔不断地调用函数,而不管函数是否已经完成。

使用您上面的示例代码,您可以将 init 函数更新为:

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

to:

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持续地触发打断。

希望这能帮到你!


3

尝试不使用匿名函数来完成此操作。例如:

function draw()
{
    return true;
}

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

它还是以同样的方式运作吗?

3
这不会起作用,对吗?你需要把函数传递给 setInterval 而不是返回值吗? - Joseph Adams
3
他不是在执行绘画操作,而是传递引用。 - BradLaney
没有在draw后面加上(),它只是一个引用。 - Steven Don

3

Chrome几乎没有因为你的程序而受到任何内存压力(1.23MB在今天来说是非常低的内存使用),因此它可能认为不需要积极地进行垃圾回收。如果您修改程序以使用更多内存,则将看到垃圾收集器启动。例如,尝试执行以下操作:

<!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

3
他的问题是“为什么我的简单程序不断分配内存,导致垃圾收集” - goat
是的,垃圾收集没问题。也许我应该换一种表述方式来问:为什么一个简单的计时器方法会分配这么多内存?如果我们能够找出原因,就可以找到编写友好垃圾收集代码的方法,减少GC调用,这对于无闪烁动画至关重要... - matahari
调用函数需要内存。我没有提到这一点,因为我认为这是显而易见的。matahari,不要担心这个问题。“过早优化是万恶之源”:http://en.wikiquote.org/wiki/Donald_Knuth#Computer_Programming_as_an_Art_.281974.29 - allyourcode
6
这不是过早的优化,他正在进行动画制作。在动画循环期间浪费地分配内存不仅可能会影响帧率,而且主要问题是当垃圾收集器启动时会导致卡顿/闪烁(用户代码在垃圾收集器运行时不执行,因此您的动画会暂停一下)。 - goat

2

看起来没有内存泄漏。只要垃圾回收后内存使用量再次降低,并且总体内存使用量没有平均上升,就没有泄漏。

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

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

每次匿名函数执行时都会分配一些内存。当这些分配累积到某个阈值时,GC将启动并清理以将您带回基本水平。这个循环将继续下去,直到您关闭它。这是预期的行为。


值得注意的是,JavaScript 在堆上放置堆栈帧有些不寻常。 - ICR
它还需要分配参数对象。 - ICR

2
我也遇到了同样的问题。客户向我报告说,他的电脑内存每次都在增加。起初,我觉得很奇怪一个 Web 应用程序竟然会导致这种情况,尽管它只是被简单的浏览器访问。我注意到这种情况只发生在 Chrome 上。
然而,我和我的合作伙伴开始调查,通过 Chrome 的开发者工具和任务管理器,我们能够看到客户报告的内存增加。
然后,我们发现一个 jQuery 函数(request animation frame)一遍又一遍地加载,导致系统内存不断增加。之后,我们通过这篇文章发现,一个 jQuery 倒计时正在做这件事,因为它内部有一个“SETINTERVAL”,每次更新我的应用程序布局中的日期。
由于我正在使用 ASP.NET MVC,我只需从 BundleConfig 中退出这个 jquery 脚本倒计时,并从我的布局中用以下代码替换我的时间倒计时:
@(DateTime.Now.ToString("dd/MM/yyyy HH:mm"))

0

我遇到了类似的问题在这篇帖子中,尝试了不同的实现setTimeoutsetInterval的方式都没有解决问题。

解决方案是增加setTimeoutsetInterval的间隔时间,让JS引擎有时间“呼吸”,因为垃圾回收原则通常在JavaScript引擎空闲时的低活动期间触发。

顺便提一下:JavaScript主要是单线程的。这意味着JavaScript代码在一个序列或执行线程中执行。

更多细节可以查看我的问题


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