web-dev-qa-db-ja.com

JavaScriptのsetInterval()メソッドはメモリリークを引き起こしますか?

現在、JavaScriptベースのアニメーションプロジェクトを開発しています。

setInterval()setTimeout()、さらにはrequestAnimationFrameを適切に使用すると、リクエストなしでメモリが割り当てられ、ガベージコレクションの呼び出しが頻繁に発生することに気付きました。より多くのGC呼び出し=フリッカー:

例えば; Google Chromeでinit()を呼び出して次の---(シンプルコードを実行すると、メモリ割り当て+ガベージコレクションは最初の20〜30秒間は問題ありません...

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

function draw()
{
    return true
}

どういうわけか、1分程度で、割り当てられたメモリの奇妙な増加が始まります! init()は一度だけ呼び出されるため、割り当てられたメモリサイズが増加する理由は何ですか?

(編集:chromeスクリーンショットがアップロードされました)

chrome screenshot

注#1:はい、次のsetInterval()の前にclearInterval()を呼び出してみました。問題は同じままです!

注#2:問題を切り分けるために、上記のコードはシンプルで愚かです。

61
matahari

編集: ユーリーの答え の方が良い。


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/

どうやらsetTimeoutsetIntervalは公式にはJavascriptの一部ではないようです(したがって、v8の一部ではありません)。実装は実装者に任されています。 node.jsのsetIntervalなどの実装 をご覧になることをお勧めします

51
Luqmaan

ここでの問題はコード自体にあるのではなく、漏れはありません。これは、タイムラインパネルの実装方法が原因です。タイムラインがイベントを記録するとき、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

28

関数呼び出しを行うたびに、 スタックフレーム が作成されます。他の多くの言語とは異なり、Javascriptは他のすべてと同様に、スタックフレームをヒープに格納します。つまり、50ミリ秒ごとに実行している関数を呼び出すたびに、新しいスタックフレームがヒープに追加されます。これは合計され、最終的にガベージコレクションされます。

Javascriptの仕組みを考えると、それはやむを得ないことです。それを軽減するために本当にできる唯一のことは、スタックフレームを可能な限り小さくすることです。これは、すべての実装が行うと確信しています。

12
ICR

SetIntervalとちらつきについてのコメントに返信したいです。

SetInterval()、setTimeout()、requestAnimationFrameを適切に使用すると、リクエストなしでメモリが割り当てられ、ガベージコレクションの呼び出しが頻繁に発生することに気付きました。より多くのGC呼び出し=フリッカー:

SetInterval呼び出しを、less evil setTimeoutに基づく自己呼び出し関数に置き換えてみてください。 Paul Irishは、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の継続的な起動によって中断されないので、デバッグは少し簡単です

お役に立てれば!

6
mrdc

無名関数なしでこれを試してください。例えば:

function draw()
{
    return true;
}

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

それでも同じように動作しますか?

3
antimeme

Chromeは、プログラムからのメモリプレッシャーをほとんど認識していません(1.23 MBは、今日の標準ではメモリ使用量が非常に少ないため)。より多くのメモリを使用するようにプログラムを変更すると、ガベージコレクターが起動します。これを試して:

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

メモリリークは発生していないようです。メモリ使用量がGC後に再び減少し、全体的なメモリ使用量が平均して増加傾向にない限り、リークはありません。

ここで私が見ている「本当の」質問は、setIntervalが実際に動作するためにメモリを使用していることであり、何かを割り当てる必要があるようには見えません。実際には、いくつかのことを割り当てる必要があります。

  1. 無名関数とdraw()ルーチンの両方を実行するには、いくつかのスタックスペースを割り当てる必要があります。
  2. 呼び出し自体を実行するために一時データを割り当てる必要があるかどうかはわかりません(おそらくそうではありません)
  3. draw()からのtrue戻り値を保持するために、少量のストレージを割り当てる必要があります。
  4. 内部的に、setIntervalは追加のメモリを割り当てて、繰り返し発生するイベントのスケジュールを変更する場合があります(内部的にどのように機能するかはわかりません。既存のレコードを再利用する場合があります)。
  5. JITは、そのメソッドをトレースしようとする場合があります。これにより、トレースといくつかのメトリックに追加のストレージが割り当てられます。 VMは、このメソッドが小さすぎてトレースできないと判断される場合があります。トレースをオンまたはオフにするためのすべてのしきい値が正確にはわかりません。 VM「ホット」として識別するために、JITコンパイル済みマシンコードを保持するためにさらに多くのメモリを割り当てることがあります(その後、生成されたマシンコードは割り当てられるため、平均メモリ使用量が減少すると予想されますほとんどの場合、より少ないメモリ)

匿名関数を実行するたびに、メモリが割り当てられます。これらの割り当てがしきい値に達すると、GCが起動してクリーンアップし、ベースレベルに戻ります。サイクルは、停止するまでこのように続きます。これは予想される動作です。

2
awhitworth

私も同じ問題を抱えています。クライアントは、コンピューターのメモリがどんどん増えていると報告しました。最初は、単純なブラウザでアクセスされていても、Webアプリでそれができるのは本当に奇妙だと思いました。これはChromeでのみ発生していることに気付きました。

しかし、私はパートナーと一緒に調査を開始し、Chromeとマネージャータスクを使用して、クライアントから報告されたメモリの増加を確認できました。

次に、jquery関数(アニメーションフレームの要求)がロードされ、システムメモリが何度も増加していることがわかります。その後、この投稿のおかげで、jqueryカウントダウンがそれを行っていました。これは、毎回アプリのレイアウトの日付を更新する「SETINTERVAL」内にあるためです。

ASP.NET MVCで作業しているときに、BundleConfigおよびレイアウトからこのjqueryスクリプトカウントダウンを終了し、時間のカウントダウンを次のコードに置き換えました。

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