JavaScript
 Computer >> コンピューター >  >> プログラミング >> JavaScript

JavaScriptのメモリリークとは?代表的な原因と対策を徹底解説


JavaScriptにおけるメモリリークの基礎知識

JavaScriptは「ガベージコレクション(GC)」と呼ばれる自動メモリ管理機構を備えた言語です。変数を宣言すると自動的にメモリが割り当てられ、その変数への参照がなくなれば、割り当てられたメモリは自動的に解放されます。

しかし、このメモリ解放の過程で問題が発生することがあります。不要になったはずのオブジェクトが参照を保持され続け、解放されずにメモリ上に残ってしまう状態、それがメモリリークです。メモリリークはページの動作の重さやクラッシュなど、多くのメモリ関連トラブルの原因となります。

ここでは、JavaScriptで特によく見られる代表的なメモリリークのパターンを2つ紹介します。

1)意図しないグローバル変数

宣言されていない変数を参照した場合、JavaScriptはその変数をグローバルオブジェクト(ブラウザでは window オブジェクト)に新しく作成してしまいます。

例えば、次のコードでは languages 変数は myArray 関数内でのみ使うことを想定しています。しかし、varletconst を付けずに代入しているため、暗黙的にグローバル変数が生成されます。

例1:var宣言なしの代入

function myArray(arg) {
    languages = "[javascript,.....]"; // windowオブジェクトに作成される
}

さらに注意すべきは、this キーワードを使ったケースです。通常の関数呼び出しでは this はグローバルオブジェクトを指すため、次のように書くとこちらもグローバル変数を作り出してしまいます。

例2:thisキーワードによるグローバル汚染

function myArray(arg) {
    this.languages = "[javascript,.....]"; // thisはグローバルオブジェクトを指す
}

グローバル変数のスコープはプログラム全体に及ぶため、たとえ不要になってもページが表示されている間ずっとメモリに残り続けます。スコープの終了時に動的メモリを回収するガベージコレクタにとっても、これは回収対象にならない未使用オブジェクト、つまりメモリリークを生み出します。グローバル変数が増えるほど、リークも大きくなるのです。

対策ポイント

  • 変数は必ず letconst で宣言する
  • ファイルの先頭で 'use strict'; を記述し、厳格モードを有効にする(宣言なしの変数への代入がエラーになる)

2)クロージャによるメモリリーク

クロージャとは、外側の関数の変数(スコープ)にアクセスできる内側の関数のことです。クロージャは、外側の関数の実行が終わった後でも、内側の関数から外側のスコープへアクセスし続けられるという特徴を持っています。

この仕組みは強力である一方、内側の関数が実際にはその変数を参照していなくても、宣言された変数が内側のネストされた関数から自動的に利用可能な状態となり、メモリ上に残り続けることでメモリリークが発生します。

以下の例を見てみましょう。クロージャ内のすべての内部関数は同じコンテキストを共有します。innFun() は、外側の関数が返す function() {} と同じコンテキストを共有しています。

コード例:setIntervalによる継続的なリーク

var newvalue;
function outFun() {
    var array = new Array(1000000); // 大きな配列を確保
    var value = newvalue;
        function innFun() {
            if (value) return array;
        }
    return function () {};
}
setInterval(function () {
    newvalue = outFun();
}, 3);

このコードでは、3ミリ秒ごとに outFun が呼び出されるたびに、新しい値がグローバル変数 newvalue に代入されていきます。

function() {} への参照が存在する限り、共有されたスコープは維持されます。そして、大きな配列は内側の関数(innFun)の一部であるため、内側の関数が一度も呼ばれなくても配列はメモリに保持され続けます

さらに、外側の関数(outFun)を呼び出すたびに、前回の function() {} が新しい関数の値(変数)として保存されます。その結果、以前に共有していたスコープも保持し続けなければなりません。したがって、n回目の呼び出しの時点で、(n−1)回目の呼び出しで生成された配列はガベージコレクションの対象になりません。

このプロセスは最終的にメモリが使い果たされるまで続きます。

対策ポイント

  • setInterval を使い終えたら必ず clearInterval で解除する
  • クロージャ内で大きなデータ(巨大な配列やDOMノードなど)を保持しない設計にする
  • 不要になった参照には null を代入して参照を切る

まとめ

JavaScriptはガベージコレクションによってメモリ管理を自動化しているものの、書き方によっては不要なオブジェクトが解放されず、メモリリークが発生します。特に「意図しないグローバル変数」と「クロージャ」は代表的な原因です。厳格モードの活用や適切な変数宣言、タイマーの解除などを心がけることで、これらの問題は十分に防げます。大規模なアプリケーション開発では、Chrome DevTools のメモリプロファイラなどを活用して、定期的にメモリの状態を確認することをおすすめします。

  1. JavaScriptの「is not a constructor」エラーとは?原因と解決方法をわかりやすく解説

    JavaScriptで「TypeError: 〇〇 is not a constructor(コンストラクターではありません)」というエラーが発生するのは、コンストラクターとして呼び出せないオブジェクトや変数に対して new 演算子を使った場合です。 たとえば、数値や文字列などのプリミティブ値、あるいはアロー関数は new によるインスタンス化に対応していないため、このエラーがスローされます。 エラーが発生するコード例 以下は、数値型の変数をコンストラクターとして呼び出し、「not a constructor」エラーを発生させるサンプルコードです。 <!DOCTYPE html>

  2. JavaScriptでのメソッド借用:call()・apply()・bind()の使い方を解説

    JavaScriptでは、call()、apply()、bind()という3つのメソッドを使うことで、あるオブジェクトが持つメソッドを別のオブジェクトが「借用」できます。これは、本来そのメソッドを持たないオブジェクトに対して、thisの参照先を差し替えてメソッドを実行できる便利なテクニックです。 3つのメソッドの違い call():引数をカンマ区切りで個別に渡し、即座に関数を実行します。 apply():引数を配列(または配列風オブジェクト)として渡し、即座に関数を実行します。 bind():thisを固定した新しい関数を返すだけで、即座には実行されません。返された関数は後から好きなタイミン