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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/14034107/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-24 15:30:16  来源:igfitidea点击:

Does JavaScript setInterval() method cause memory leak?

javascriptmemory-leakssetinterval

提问by matahari

Currently developing a JavaScript based animation project.

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

I have noticed that, proper use of setInterval(), setTimeout()and even requestAnimationFrameallocates memory without my request, and causes frequent garbage collection calls. More GC calls = flickers :-(

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

For instance; when I execute the following simple codeby calling init() in Google Chrome, memory allocation + garbage collection is fine for the first 20-30 seconds...

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

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

function draw()
{
    return true
}

Somehow, within a minute or so, starts a strange increase in allocated memory! Since init() is called only for once, what is the reason for the increase in allocated memory size?

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

(Edit: chrome screenshot uploaded)

(编辑:上传chrome截图)

chrome screenshot

镀铬屏幕截图

NOTE #1: Yes, I have tried calling clearInterval() before the next setInterval(). Problem remains the same!

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

NOTE #2: In order to isolate the problem, I'm keeping the above code simple and stupid.

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

回答by Luqmaan

EDIT: Yury's answeris better.

编辑:尤里的答案更好。



tl;dr IMO there is no memory leak. The positive slope is simply the effect of setInterval and setTimeout. The garbage is collected, as seen by sawtooth patterns, meaning by definition there is no memory leak. (I think).

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

I'm not sure there is a way to work around this so-called "memory leak." In this case, "memory leak" is referring to each call to the setInterval function increasing the memory usage, as seen by the positive slopes in the memory profiler.

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

The reality is that there is no actual memory leak: the garbage collector is still able to collect the memory. Memory leak by definition "occurs when a computer program acquires memory but fails to release it back to the operating system."

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

As shown by the memory profiles below, memory leak is not occurring. The memory usage is increasing with each function call. The OP expects that because this is the same function being called over and over, there should be no memory increase. However, this is not the case. Memory is consumed with each function call. Eventually, the garbage is collected, creating the sawtooth pattern.

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

I've explored several ways of rearranging the intervals, and they all lead to the same sawtooth pattern (although some attempts lead to garbage collection never happening as references were retained).

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

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

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

a();

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

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/

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/

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/

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

Apparently setTimeoutand setIntervalare not officially parts of Javascript (hence they are not a part of v8). The implementation is left up to the implementer. I suggest you take a look at the implementation of setInterval and such in node.js

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

回答by Yury Semikhatsky

The problem here is not in the code itself, it doesn't leak. It is because of the way Timeline panel is implemented. When Timeline records events we collect JavaScript stack traces on each invocation of setInterval callback. The stack trace is first allocated in JS heap and then copied into native data structures, after the stack trace is copied into the native event it becomes garbage in the JS heap. This is reflected on the graph. Disabling the following call http://trac.webkit.org/browser/trunk/Source/WebCore/inspector/TimelineRecordFactory.cpp#L55makes the memory graph flat.

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

There is a bug related to this issue: https://code.google.com/p/chromium/issues/detail?id=120186

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

回答by ICR

Each time you make a function call, it creates a stack frame. Unlike lots of other languages, Javascript stores the stack frame on the heap, just like everything else. This means that every time you call a function, which you're doing every 50ms, a new stack frame is being added to the heap. This adds up and is eventually garbage collected.

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

It's kinda unavoidable, given how Javascript works. The only thing that can really be done to mitigate it is make the stack frames as small as possible, which I'm sure all the implementations do.

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

回答by mrdc

I wanted to respond to your comment about setInterval and flickering:

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

I have noticed that, proper use of setInterval(), setTimeout() and even requestAnimationFrame allocates memory without my request, and causes frequent garbage collection calls. More GC calls = flickers :-(

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

You might want to try replacing the setInterval call with a less evilself-invoking function based on setTimeout. Paul Irish mentions this in the talk called 10 things I learned from the jQuery source (video here, notes heresee #2). What you do is replace your call to setInterval with a function that invokes itself indirectly through setTimeout after it completes the work it's supposed to do. To quote the talk:

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

Many have argued that setInterval is an evil function. It keeps calling a function at specified intervals regardless of whether the function is finished or not.

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

Using your example code above, you could update your init function from:

使用上面的示例代码,您可以从以下位置更新 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);
}

This should help a bit because:

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

  1. draw() won't be called again by your rendering loop until it's completed
  2. as many of the above answers point out, all of the uninterrupted function calls from setInterval do put overhead on the browser.
  3. debugging is a bit easier as you're not interrupted by the continued firing of setInterval
  1. draw() 在完成之前不会被渲染循环再次调用
  2. 正如上面的许多答案所指出的那样,来自 setInterval 的所有不间断函数调用都会给浏览器带来开销。
  3. 调试更容易一些,因为您不会被 setInterval 的持续触发打断

Hope this helps!

希望这可以帮助!

回答by allyourcode

Chrome is hardly seeing any memory pressure from your program (1.23 MB is very low memory usage by today's standards), so it probably does not think it needs to GC aggressively. If you modify your program to use more memory, you will see the garbage collector kick in. e.g. try this:

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>

When I run this, I get a saw tooth memory usage pattern, peaking bellow around 13.5MB (again, pretty small by today's standards).

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

PS: Specifics of my browsers:

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

回答by antimeme

Try doing this without the anonymous function. For example:

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

function draw()
{
    return true;
}

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

Does it still behave the same way?

它的行为仍然相同吗?

回答by awhitworth

There does not appear to be a memory leak. So long as the memory usage decreases again after GC, and the overall memory usage does not trend upward on average, there is no leak.

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

The "real" question I'm seeing here is that setIntervaldoes indeed use memory to operate, and it doesn't look like it should be allocating anything. In reality it needs to allocate a few things:

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

  1. It will need to allocate some stack space to execute both the anonymous function and the draw() routine.
  2. I don't know if needs to allocate any temporary data to perform the calls themselves (probably not)
  3. It needs to allocate a small amount of storage to hold that truereturn value from draw().
  4. Internally, setInterval may allocate additional memory to re-schedule a reoccurring event (I don't know how it works internally, it may re-use the existing record).
  5. The JIT may try to trace that method, which would allocate additional storage for the trace and some metrics. The VM may determine this method is too small to trace it, I don't know exactly what all the thresholds are for turning tracing on or off. If you run this code long enough for the VM to identify it as "hot", it may allocate even more memory to hold the JIT compiled machine code (after which, I would expect average memory usage to decrease, because the generated machine code should allocate less memory in most cases)
  1. 它需要分配一些堆栈空间来执行匿名函数和 draw() 例程。
  2. 我不知道是否需要分配任何临时数据来自己执行调用(可能不需要)
  3. 它需要分配少量存储空间来保存true来自draw().
  4. 在内部,setInterval 可能会分配额外的内存来重新安排重复发生的事件(我不知道它内部是如何工作的,它可能会重新使用现有记录)。
  5. JIT 可能会尝试跟踪该方法,这将为跟踪和一些指标分配额外的存储空间。VM 可能确定此方法太小而无法跟踪它,我不确切知道打开或关闭跟踪的所有阈值是多少。如果你运行这段代码足够长的时间让 VM 将它识别为“热”,它可能会分配更多的内存来保存 JIT 编译的机器代码(之后,我预计平均内存使用量会减少,因为生成的机器代码应该大多数情况下分配较少的内存)

Every time your anonymous function executes there is going to be some memory allocated. When those allocations add up to some threshold, the GC will kick in and clean up to bring you back down to a base level. The cycle will continue like this until you shut it off. This is expected behavior.

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

回答by Paola Bruni

I also have the same problem. The client reported me that the memory of its computer was increasing every time more and more. At first I thought it was really strange that a web app could make that even though it was accessed by a simple browser. I noticed that this happened only in Chrome.

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

However, I started with a partner to investigate and through the developer tools of Chrome and the manager task we could see the memory increase the client had reported me.

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

Then we see that a jquery function (request animation frame) was loaded over and over increasing the system memory. After that, we saw thanks to this post, a jquery countdown was doing that, because it has inside a "SETINTERVAL" that each time was updating the date in my app's layout.

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

As I am working with ASP.NET MVC, I just quit this jquery script countdown from the BundleConfig, and from my layout, replacing my time countdown with the following code:

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

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