JavaScriptメモリ管理





良い一日、友達!



ほとんどの場合、JavaScript開発者として、メモリの操作について心配する必要はありません。エンジンは私たちのためにそれを行います。



しかし、ある日、「メモリリーク」と呼ばれる問題が発生します。これは、JavaScriptでメモリがどのように割り当てられているかを知ることによってのみ解決できます。



この記事では、メモリ割り当てとガベージコレクションがどのように機能するか、およびメモリリークに関連する一般的な問題のいくつかを回避する方法について説明します。



メモリライフサイクル



変数または関数を作成すると、JavaScriptエンジンはそれにメモリを割り当て、不要になったときに解放します。



メモリの割り当ては、メモリ内の特定のスペースを予約するプロセスであり、メモリを解放すると、他の目的に使用できるようにそのスペースが解放されます。



変数または関数が作成されるたびに、メモリは次の段階を経ます。







  • メモリ割り当て-エンジンは、作成されたオブジェクトにメモリを自動的に割り当てます
  • メモリ使用量-メモリへのデータの読み取りと書き込みは、変数からのデータの書き込みと読み取りにすぎません。
  • メモリの解放-このステップもエンジンによって自動的に実行されます。メモリが解放されると、他の目的に使用できます。


ヒープとスタック



次の質問は、メモリとはどういう意味ですか?データは実際にどこに保存されていますか?



エンジンには、ヒープとスタックの2つの場所があります。ヒープとスタックは、エンジンがさまざまな目的で使用するデータ構造です。



スタック:静的メモリ割り当て







この例のすべてのデータはプリミティブであるため、スタックに格納されます。



スタックは、静的データを格納するために使用されるデータ構造です。静的データは、コードのコンパイル段階でエンジンにサイズがわかっているデータです。JavaScriptでは、そのようなデータはプリミティブ(文字列、数値、ブール値、未定義、およびnull)であり、オブジェクトと関数を指す参照です。



エンジンはデータのサイズが変更されないことを認識しているため、値ごとに固定サイズのメモリを割り当てます。コードを実行する前にメモリを割り当てるプロセスは、静的メモリ割り当てと呼ばれます。エンジンは固定サイズのメモリを割り当てるため、このサイズには一定の制限があり、ブラウザに大きく依存します。



ヒープ:動的メモリ割り当て



ヒープは、オブジェクトと関数を格納するためのものです。スタックとは異なり、エンジンはオブジェクトに固定サイズのメモリを割り当てません。メモリは必要に応じて割り当てられます。このメモリ割り当ては動的と呼ばれます。これが小さな比較表です:



スタック ヒープ
プリミティブ値と参照 オブジェクトと機能
サイズはコンパイル時にわかります サイズは実行時に既知です
割り当てられた固定メモリ 各オブジェクトのメモリサイズは制限されていません


の例



いくつかの例を見てみましょう。



  const person = {
    name: "John",
    age: 24,
  };


エンジンは、ヒープ上のこのオブジェクトにメモリを割り当てます。ただし、プロパティ値はスタックに保存されます。



  const hobbies = ["hiking", "reading"];


配列はオブジェクトであるため、ヒープに格納されます



  let name = "John";
  const age = 24;

  name = "John Doe";
  const firstName = name.slice(0, 4);


プリミティブは不変です。これは、元の値を変更する代わりに、JavaScriptが新しい値を作成することを意味します。



リンク



すべての変数はスタックに保存されます。非プリミティブ値の場合、スタックはオブジェクトへの参照をヒープに格納します。ヒープ上のメモリが乱れています。これが、スタックにリンクが必要な理由です。リンクはアドレス、オブジェクトは特定のアドレスの家と考えることができます。







上の画像では、さまざまな値がどのように保存されているかを確認できます。personとnewPersonが同じオブジェクトを指していることに注意してください



の例



  const person = {
    name: "John",
    age: 24,
  };


これにより、ヒープ上に新しいオブジェクトが作成され、スタック上にそのオブジェクトへの参照が作成されます



ガベージコレクション



エンジンは、変数または関数が使用されなくなったことを認識するとすぐに、占有していたメモリを解放します。



実際、未使用のメモリを解放するという問題は解決できません。それを解決するための完全なアルゴリズムはありません。



この記事では、これまでの最良のソリューションを提供する2つのアルゴリズム、参照カウントのガベージコレクションとマークアンドスイープについて説明します。



参照カウントによるごみ収集



ここではすべてが単純です-参照ポイントがメモリから削除されていないオブジェクト。例を見てみましょう。線はリンクを表します。







このオブジェクトのみがスタックで参照されるため、「hobbies」オブジェクトのみがヒープに残ることに注意してください。



循環リンク



このガベージコレクションメソッドの問題は、循環参照を定義できないことです。これは、2つ以上のオブジェクトが相互にポイントしているが、外部参照がない状況です。それら。これらのオブジェクトには外部からアクセスできません。



  const son = {
    name: "John",
  };

  const dad = {
    name: "Johnson",
  };

  son.dad = dad;
  dad.son = son;

  son = null;
  dad = null;






オブジェクト「son」と「dad」は相互に参照しているため、参照カウントアルゴリズムはメモリを解放できません。ただし、これらのオブジェクトは外部コードでは使用できなくなりました。



タグ付けとクリーニングのアルゴリズム



このアルゴリズムは、循環参照の問題を解決します。オブジェクトを指す参照をカウントする代わりに、ルートオブジェクトからのオブジェクトのアクセス可能性を決定します。ルートオブジェクトは、ブラウザの「ウィンドウ」オブジェクト、またはNode.jsの「グローバル」オブジェクトです。







アルゴリズムは、オブジェクトを到達不能としてマークし、それらを削除します。したがって、循環参照はもはや問題ではありません。上記の例では、オブジェクト「dad」と「son」はルートオブジェクトから到達できません。それらはゴミ箱としてマークされ、削除されます。問題のアルゴリズムは、2012年以降すべての最新のブラウザーに実装されています。それ以降に行われた改善は、実装とパフォーマンスの改善に関するものですが、アルゴリズムのコアアイデアではありません。



妥協



自動ガベージコレクションにより、メモリ管理に時間を無駄にすることなく、アプリケーションの構築に集中できます。ただし、すべてに代償が伴います。



メモリ使用量



アルゴリズムがメモリが使用されなくなったと判断するのに時間がかかることを考えると、JavaScriptアプリケーションは実際に必要な量よりも多くのメモリを使用する傾向があります。



オブジェクトがガベージとしてマークされている場合でも、コレクターは、プログラムフローをブロックしないように、オブジェクトを収集するタイミングを決定する必要があります。アプリケーションでメモリ使用量をできるだけ効率的にする必要がある場合は、低レベルのプログラミング言語を使用することをお勧めします。ただし、そのような言語には独自のトレードオフがあることに注意してください。



パフォーマンス



ガベージコレクションアルゴリズムは定期的に実行され、未使用のオブジェクトをクリーンアップします。問題は、開発者として、これがいつ発生するかを正確に知らないことです。大量のガベージコレクションまたは頻繁なガベージコレクションは、ある程度の処理能力を必要とするため、パフォーマンスに影響を与える可能性があります。ただし、これは通常、ユーザーと開発者が気付かないうちに発生します。



メモリリーク



最も一般的なメモリリークの問題を簡単に見てみましょう。



グローバル変数



キーワード(var、let、またはconst)のいずれかを使用せずに変数を宣言すると、その変数はグローバルオブジェクトのプロパティになります。



  users = getUsers();


strictモードでコードを実行すると、これを回避できます。



意図的にグローバル変数を宣言することもあります。この場合、そのような変数によって占有されているメモリを解放するには、値「null」を割り当てる必要があります。



  window.users = null;


忘れられたタイマーとコールバック



タイマーとコールバックを忘れると、アプリケーションのメモリ使用量が劇的に増加する可能性があります。特に、イベントハンドラーとコールバックが動的に追加されるシングルページアプリケーション(SPA)を作成する場合は、注意が必要です。



忘れられたタイマー



  const object = {};
  const intervalId = setInterval(function () {
    // ,   ,      ,
    //   ,     
    doSomething(object);
  }, 2000);


上記のコードは、2秒ごとに関数を実行します。タイマーが不要になった場合は、次の方法でキャンセルする必要があります。



  clearInterval(intervalId);


これはSPAにとって特に重要です。タイマーが使用されていない別のページに移動しても、バックグラウンドで実行されます。



忘れられたコールバック



後で削除するボタンクリックのハンドラーを登録するとします。実際、これはもはや問題ではありませんが、不要になったハンドラーを削除することをお勧めします。



  const element = document.getElementById("button");
  const onClick = () => alert("hi");

  element.addEventListener("click", onClick);

  element.removeEventListener("click", onClick);
  element.parentNode.removeChild(element);


DOM外のリンク



このメモリリークは前のものと同様であり、DOM要素をJavaScriptに格納するときに発生します。



  const elements = [];
  const element = document.getElementById("button");
  elements.push(element);

  function removeAllElements() {
    elements.forEach((item) => {
      document.body.removeChild(document.getElementById(item.id));
    });
  }


これらの要素のいずれかを削除する場合は、それも配列から削除する必要があります。そうしないと、そのようなアイテムはガベージコレクターによって削除できません。



  const elements = [];
  const element = document.getElementById("button");
  elements.push(element);

  function removeAllElements() {
    elements.forEach((item, index) => {
      document.body.removeChild(document.getElementById(item.id));
      elements.splice(index, 1);
    });
  }


何か面白いものを見つけていただければ幸いです。清聴ありがとうございました。



All Articles