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

IT技术 javascript memory-leaks setinterval
2021-03-16 03:19:12

目前正在开发一个基于 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:为了隔离问题,我将上面的代码保持简单和愚蠢。

6个回答

编辑:尤里的答案更好。


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/

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

@inspectahdeck 我认为你是对的,这只是在给定时间间隔内每个函数调用所需的常规内存分配。
2021-04-17 03:19:12
@ user1928710 我不好。我现在正在研究正确的问题。我的猜测是函数调用不断被推入堆栈。
2021-04-22 03:19:12
是的,正如我在原始消息中发布的那样,我已经知道垃圾收集。有用。我不明白的是,为什么一个定时器方法,比如 setInterval() ,一直在吃内存?从您的 JSFiddle,我可以看到从 1.1 分钟到 4.8 分钟,内存分配上升(不断增加)!它请求的更多内存 = 触发更多 GC 调用!为了停止不必要的 GC 调用,需要“驯服” setInterval() 以便停止分配内存......
2021-04-24 03:19:12
希望我们可以从 Chrome 论坛 ( code.google.com/p/chromium/issues/detail?id=167647 )得到官方答案,并了解它是否只是定时器方法的(ir)常规内存分配请求......
2021-05-03 03:19:12
我怀疑这是堆栈,因为您在这里处理的不是递归,而是异步操作。
2021-05-06 03:19:12

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

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

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

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

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

您可能想尝试使用基于 setTimeout不那么邪恶的自调用函数替换 setInterval 调用保罗爱尔兰提到这在谈话称为10件事情我从jQuery源教训(视频这里,音符在这里看到#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 的持续触发打断

希望这可以帮助!

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
他的问题是“为什么我的简单程序不断分配内存,导致垃圾收集”
2021-04-24 03:19:12
调用函数需要内存。我没有提到这一点,因为我认为这很明显。matahari,不要担心这个。“过早的优化是万恶之源”:en.wikiquote.org/wiki/...
2021-04-26 03:19:12
是的,垃圾收集是可以的。也许我应该重新表述我的问题:为什么一个简单的计时器方法会分配这么多内存?如果我们能弄清楚这一点,我们就可以找到一种编写垃圾收集友好代码的方法,使用最少的 GC 调用。无闪烁动画所必需的...
2021-04-30 03:19:12
这不是过早的优化,他在做动画,当你在动画循环期间浪费地分配内存时,你不仅可能会损害帧率,而且主要问题是当垃圾收集器启动时你会导致口吃/闪烁(用户代码没有在垃圾收集器运行时执行 - 因此您的动画会短暂停止)。
2021-05-13 03:19:12