ホームページ  >  記事  >  ウェブフロントエンド  >  JavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法

JavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法

coldplay.xixi
coldplay.xixi転載
2020-12-09 17:12:492658ブラウズ

#javascriptこのコラムでは、もう 1 つの重要なトピックであるメモリ管理について説明します

JavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法

#関連する無料学習の推奨事項: javascript(ビデオ)

We will Another議論される重要なトピックはメモリ管理です。メモリ管理は、日常的に使用されるプログラミング言語の成熟度と複雑さが増すため、開発者によって見落とされがちです。また、JavaScript でのメモリ リークに対処する方法に関するいくつかのヒントも提供します。SessionStack でこれらのヒントに従うことで、SessionStack がメモリ リークを引き起こさず、統合 Web アプリケーションのメモリ消費量が増加しないことが保証されます。


もっと質の高い記事を読みたい場合は、GitHub ブログをクリックしてください。毎年数百の質の高い記事があなたを待っています。

概要

C などのプログラミング言語には、malloc() や free() などの低レベルのメモリ管理プリミティブがあります。開発者はこれらのプリミティブを使用して、オペレーティング システムからメモリを明示的に割り当てたり解放したりできます。

JavaScript は、オブジェクト (オブジェクト、文字列など) の作成時にメモリを割り当て、使用されなくなったときにメモリを「自動的に」解放します。このプロセスはガベージ コレクションと呼ばれます。この一見「自動的」に見えるリソースの解放は、JavaScript (およびその他の高級言語) 開発者に、メモリ管理をそれほど気にしなくてもよいという誤った印象を与えるため、混乱の原因となっています。これは考えです。大きな間違いです。

高級言語を扱う場合でも、開発者はメモリ管理を理解している (または少なくとも基本を知っている) 必要があります。場合によっては、自動メモリ管理にいくつかの問題 (ガベージ コレクターのバグや実装の制限など) が存在するため、開発者はこれらの問題を理解し、正しく処理できるようにする (または、最小限のリソースで維持できる適切な解決策を見つける必要がある) 必要があります。エフォートコード)。

メモリ ライフ サイクル

どのプログラミング言語を使用しても、メモリ ライフ サイクルは同じです:

JavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法

概要は次のとおりです。はじめに メモリのライフ サイクルの各段階を見てみましょう:

  • メモリの割り当て — メモリはオペレーティング システムによって割り当てられ、プログラムで使用できるようになります。低レベル言語 (C など) では、これは明示的に実行される操作であり、開発者自身が処理する必要があります。ただし、高級言語では、システムが自動的に組み込み関数を割り当てます。
  • Using Memory — これは、プログラムが実際に使用する前に割り当てられるメモリです。割り当てられた変数がコード内で使用されると、読み取りと書き込みが発生します。
  • メモリの解放 — 使用されなくなったすべてのメモリを解放して、メモリを解放し、再利用できるようにします。メモリ割り当て操作と同様、この操作も低レベル言語で明示的に実行する必要があります。
メモリとは何ですか?

JavaScript でメモリを紹介する前に、メモリとは何か、またメモリがどのように機能するかについて簡単に説明します。

ハードウェア レベルでは、コンピューターのメモリは多数のトリガーによってキャッシュされます。各フリップフロップには 1 ビットを保存できる複数のトランジスタが含まれており、個々のフリップフロップは一意の識別子でアドレス指定できるため、読み取りや上書きが可能です。したがって、概念的には、コンピューターのメモリ全体は、読み取りと書き込みが可能な巨大な配列とみなすことができます。

人間として、ビットの観点から考えたり計算したりするのはあまり得意ではないので、ビットをまとめて数値を表すために使用できる大きなグループに編成します。 8ビットを1バイトといいます。バイトに加えて、ワードもあります (16 ビットの場合もあれば、32 ビットの場合もあります)。

多くのものがメモリに保存されます:

    プログラムで使用されるすべての変数とその他のデータ。
  1. プログラム コード (オペレーティング システム コードを含む)。
コンパイラとオペレーティング システムがメモリ管理のほとんどを処理しますが、基礎となる管理概念をより深く理解するには、基礎となる状況を理解する必要があります。

コードをコンパイルするとき、コンパイラーは基本的なデータ型をチェックし、必要なメモリ量を事前に計算できます。次に、必要なサイズが呼び出しスタック領域内のプログラムに割り当てられます。これらの変数が割り当てられる領域はスタック領域と呼ばれます。関数が呼び出されると、そのメモリが既存のメモリの上に追加され、関数が終了すると後入れ先出し (LIFO) 順序で削除されるためです。例:

JavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法

コンパイラは必要なメモリをすぐに認識します: 4 4 × 4 8 = 28 バイト。

このコードは、整数および倍精度浮動小数点変数によって占有されるメモリ サイズを示します。しかし、約 20 年前は、整数変数は通常 2 バイトを使用し、倍精度浮動小数点変数は 4 バイトを使用していました。コードは、現在のプリミティブ データ型のサイズに依存しないでください。

コンパイラは、オペレーティング システムと対話するコードを挿入し、変数を格納するために必要なスタック バイト数を割り当てます。

上記の例では、コンパイラは各変数の正確なメモリ アドレスを認識しています。実際、変数 n に書き込むと、内部で "メモリ アドレス 4127963" のような情報に変換されます。

x[4] にアクセスしようとすると、m に関連付けられたデータがアクセスされることに注意してください。これは、配列内に存在しない要素 (配列内で実際に最後に割り当てられた要素 x[3] よりも 4 バイト大きい) にアクセスすると、m ビットほどの読み取り (または上書き) が発生する可能性があるためです。これは間違いなくプログラムの残りの部分で予測不可能な結果を​​もたらすでしょう。

JavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法

関数が他の関数を呼び出すと、各関数は呼び出しスタック上に独自のブロックを取得します。すべてのローカル変数を保持しますが、実行中にどこにあるかを記憶するプログラム カウンターも備えています。関数が完了すると、そのメモリ ブロックは別の場所で再び使用されます。

動的割り当て

残念ながら、コンパイル時に変数に必要なメモリ量がわからないと、状況が少し複雑になります。次のことを実行したいとします。

JavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法

コンパイル時には、配列が使用する必要のあるメモリの量がコンパイラにはわかりません。これは、次の値によって決定されるためです。ユーザー。

したがって、スタック上の変数にスペースを割り当てることができません。代わりに、プログラムは実行時にオペレーティング システムに適切なスペースを明示的に要求する必要があります。このメモリは ヒープ スペース から割り当てられます。静的メモリ割り当てと動的メモリ割り当ての違いを次の表にまとめます。

##コンパイル時に実行される実行時に実行スタックに割り当て##FILO (先入れ後出し)特定の割り当て順序なし

動的メモリ割り当てがどのように機能するかを完全に理解するには、ポインターについてもっと時間を費やす必要がありますが、この記事の主題から大きく逸脱する可能性があります。ここでは、ポインターに関する関連知識について詳しくは紹介しません。

JavaScript でメモリを割り当てる

最初のステップとして、JavaScript でメモリを割り当てる方法について説明します。

JavaScript により、開発者はメモリ割り当てを手動で処理する責任から解放されます。JavaScript はメモリを割り当て、値を自ら宣言します。

JavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法

特定の関数呼び出しでは、オブジェクトのメモリ割り当ても行われます。

JavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法

メソッドは新しい値またはオブジェクトを割り当てることができます。 :

JavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法

JavaScript でのメモリの使用

JavaScript で割り当てられたメモリを使用するということは、メモリの読み取りと書き込みを意味します。これは、変数または値の読み取りまたは書き込みによって実行できます。オブジェクトのプロパティの値を取得するか、関数にパラメータを渡すことによって。

メモリが不要になったら解放する

ほとんどのメモリ管理の問題はこの段階で発生します

ここで最も難しいのは、メモリの割り当てがいつ不要になるかを判断することです。 、通常、開発者はプログラム内のメモリが不要になった場所を特定し、メモリを解放する必要があります。

高級言語には、ガベージ コレクターと呼ばれるメカニズムが組み込まれています。ガベージ コレクターの役割は、割り当てられたメモリの一部が不要になった時点を検出するために、メモリの割り当てと使用状況を追跡することです。この場合、このメモリは自動的に解放されます。

残念ながら、特定のメモリが本当に必要かどうかを知るのは難しいため (アルゴリズム的に解決することはできません)、このプロセスは大まかな推定を行うだけです。

ほとんどのガベージ コレクターは、アクセスされなくなったメモリ、つまり、そのメモリを指すすべての変数がスコープ外になったメモリを収集することによって機能します。ただし、これは収集できる一連のメモリ空間を過小評価したものです。メモリの場所のどの時点でも、スコープ内にそれを指す変数が存在する可能性がありますが、その変数には再度アクセスすることはできないからです。

ガベージ コレクション

一部のメモリが本当に役立つかどうかを判断することは不可能であるため、ガベージ コレクターはこの問題を解決する方法を考え出しました。このセクションでは、主なガベージ コレクション アルゴリズムとその制限について説明し、理解します。

メモリ参照

ガベージ コレクション アルゴリズムは主に参照に依存します。

メモリ管理のコンテキストでは、オブジェクトが別のオブジェクトへのアクセス権 (暗黙的または明示的) を持っている場合、そのオブジェクトは別のオブジェクトを参照するといいます。たとえば、JavaScript オブジェクトには、そのプロトタイプへの参照 (暗黙的参照) とプロパティ値 (明示的参照) があります。

この文脈では、「オブジェクト」の概念は通常の JavaScript オブジェクトよりも広い範囲に拡張され、関数スコープ (またはグローバル字句スコープ) も含まれます。

字句スコープは、入れ子関数内で変数名がどのように解決されるかを定義します。内部関数には、親関数が返した場合でも親関数の効果が含まれます。

参照カウント ガベージ コレクション アルゴリズム

これは最も単純なガベージ コレクション アルゴリズムです。オブジェクトへの参照がない場合、次のコードに示すように、オブジェクトは「ガベージ コレクタブル」とみなされます。

JavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法

ループにより問題が発生する可能性があります

When ループする場合には制限があります。以下の例では、相互参照する 2 つのオブジェクトが作成され、ループが作成されます。関数呼び出しはスコープ外になるため、事実上役に立たず、解放できます。ただし、参照カウント アルゴリズムでは、各オブジェクトが少なくとも 1 回参照されるため、それらのオブジェクトはいずれもガベージ コレクションできないと想定されます。

JavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法

マーク アンド スイープ アルゴリズム

このアルゴリズムは、オブジェクトにアクセスできるかどうかを判断し、それによってオブジェクトが有用かどうかを知ることができます。アルゴリズムは次のもので構成されます。次の手順:

  1. ガベージ コレクターは、参照されたグローバル変数を保存するために使用される「ルート」リストを構築します。 JavaScript では、「ウィンドウ」オブジェクトはルート ノードとして使用できるグローバル変数です。
  2. その後、アルゴリズムはすべてのルートとその子をチェックし、それらをアクティブとしてマークします (つまり、それらはガベージではありません)。根が届かない場所はゴミとしてマークされます。
  3. 最後に、ガベージ コレクターは、アクティブとしてマークされていないすべてのメモリ ブロックを解放し、そのメモリをオペレーティング システムに返します。

JavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法

#「オブジェクトが参照されていない」とはオブジェクトにアクセスできないことを意味するため、このアルゴリズムは前のアルゴリズムよりも優れています。

2012 年現在、すべての最新ブラウザにはマークスイープ ガベージ コレクターが搭載されています。過去数年間に JavaScript ガベージ コレクション (世代別/増分/同時/並列ガベージ コレクション) の分野で行われたすべての改善は、アルゴリズム (マーク スイープ) の実装の改善であり、ガベージ コレクション アルゴリズム自体の改善ではありません。オブジェクトがアクセス可能かどうかを判断することが目的ですか。

この記事では、マークスイープ アルゴリズムとその最適化など、ガベージ コレクションの追跡について詳しく説明します。

ループはもはや問題ではありません

上記の最初の例では、関数呼び出しが返された後、2 つのオブジェクトはグローバル オブジェクトからアクセスできるオブジェクトによって参照されなくなります。したがって、ガベージ コレクターはそれらにアクセスできないと判断します。

JavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法

オブジェクト間に参照はありますが、ルート ノードからは参照できません。

ガベージ コレクターの直観に反する動作

ガベージ コレクターは非常に便利ですが、独自のトレードオフがあり、その 1 つは非決定性です。予想どおりではありませんが、ガベージ コレクションがいつ発生するかは実際にはわかりません。これは、場合によっては、プログラムが実際に必要以上のメモリを使用することを意味します。特に速度に敏感なアプリケーションでは、短い一時停止が目立つ場合があります。メモリが割り当てられていない場合、GC の大部分はアイドル状態になります。次のシナリオを見てください。

  1. かなり大きなインナーセットを割り当てます。
  2. これらの要素のほとんど (またはすべて) はアクセス不能としてマークされます (参照が不要になったキャッシュを指していると仮定します)。
  3. これ以上の割り当てはありません

これらのシナリオでは、ほとんどの GC はコレクションを続行しません。つまり、コレクションに使用できるアクセスできない参照が存在する場合でも、コレクターはそれらを宣言しません。これらは厳密にはリークではありませんが、それでも通常よりも多くのメモリ使用量が発生する可能性があります。

メモリ リークとは何ですか?

基本的に、メモリ リークは次のように定義できます。アプリケーションで不要になり、何らかの理由でオペレーティング システムに戻らないメモリまたは空きメモリプール。

JavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法

プログラミング言語はさまざまなメモリ管理方法をサポートしています。ただし、特定のメモリを使用するかどうかは実際には未定です。言い換えれば、メモリの一部をオペレーティング システムに返すことができるかどうかを判断できるのは開発者だけです。

プログラミング言語の中には、開発者に支援を提供するものもありますが、開発者がメモリがいつ使用されなくなるかを明確に理解することを期待するものもあります。ウィキペディアには、手動および自動のメモリ管理に関する優れた記事がいくつかあります。

4 つの一般的なメモリ リーク

1. グローバル変数

JavaScript は、未宣言の変数を興味深い方法で処理します。未宣言の変数の場合、新しい変数は次のようになります。それを参照するためにグローバル スコープ内に作成されます。ブラウザでは、グローバル オブジェクトはウィンドウです。例:

function foo(arg) {
    bar = "some text";
}

は次と同等です:

function foo(arg) {
    window.bar = "some text";
}

bar が foo 関数のスコープ内の変数を参照しているにもかかわらず、var を使用して宣言するのを忘れた場合、予期しないグローバルが作成されます。変数。この例では、単純な文字列が欠けていても大きな害はありませんが、間違いなく悪影響を及ぼします。

予期しないグローバル変数を作成するもう 1 つの方法は、これを使用することです:

function foo() {
    this.var1 = "potential accidental global";
}
// Foo自己调用,它指向全局对象(window),而不是未定义。
foo();
これを回避するには、JavaScript ファイルの先頭に「use strict」を追加します。これにより、A stricter がオンになります。グローバル変数の誤った作成を防ぐための JavaScript 解析モード。

未知のグローバル変数について話していますが、依然として明示的なグローバル変数で満たされたコードが大量にあります。定義上、これらは収集可能ではありません (空として指定されているか再割り当てされていない限り)。大量の情報を一時的に保存および処理するために使用されるグローバル変数は特に懸念されます。大量のデータを保存するためにグローバル変数を使用する必要がある場合は、完了時に必ず null を指定するか、再割り当てしてください。

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

setInterval は JavaScript でよく使用されるため、例として取り上げます。

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //每五秒会执行一次

上記のコード スニペットは、タイマーを使用して不要になったノードまたはデータを参照する方法を示しています。

レンダラーによって表されるオブジェクトは将来のある時点で削除される可能性があり、その結果、内部ハンドラー内のコード ブロック全体が不要になります。ただし、タイマーがまだアクティブであるため、ハンドラーを収集できず、その依存関係も収集できません。これは、大量のデータを格納するserverDataを収集できないことを意味します。

オブザーバーを使用する場合は、使用が終了した後 (オブザーバーが不要になるか、オブジェクトがアクセスできなくなるため)、オブザーバーを削除する明示的な呼び出しを行う必要があります。

開発者は、使用し終わったら、必ず明示的に削除する必要があります (そうしないと、オブジェクトにアクセスできなくなります)。

以前は、一部のブラウザーはこのような状況に対処できませんでした (IE6 など)。幸いなことに、最近のほとんどのブラウザはこれを自動的に実行します。リスナーを削除するのを忘れた場合でも、監視対象のオブジェクトにアクセスできなくなると、自動的にオブザーバー ハンドラーが収集されます。ただし、オブジェクトが破棄される前に、これらのオブザーバーを明示的に削除する必要があります。例:

JavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法

現在のブラウザ (IE および Edge を含む) は、これらの循環参照を即座に検出して処理できる最新のガベージ コレクション アルゴリズムを使用しています。つまり、ノードを削除する前に、removeEventListener を呼び出す必要はありません。

JQuery などの一部のフレームワークまたはライブラリは、ノードを破棄する前にリスナーを自動的に削除します (特定の API を使用する場合)。これは、IE 6 などの問題のあるブラウザで実行している場合でもメモリ リークが発生しないことを保証するライブラリ内のメカニズムによって実装されています。

3. クロージャ

クロージャは JavaScript 開発の重要な側面であり、内部関数は外部 (囲まれた) 関数の変数を使用します。 JavaScript の実行方法の詳細により、次のような方法でメモリ リークが発生する可能性があります:

JavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法

このコードは 1 つのことを実行します: 毎回 replaceThingが呼び出されると、 theThing は、大きな配列と新しいクロージャ (someMethod) を含む新しいオブジェクトを取得します。同時に、変数 unused は `originalThing を参照するクロージャを指します。

混乱していますか? 重要なことは、同じ親スコープの複数のクロージャを持つスコープを作成すると、このスコープを共有できるということです。

この場合、クロージャ someMethod に対して作成されたスコープは unused 共有できます。 未使用originalThingへの内部参照があります。 unused が一度も使用されない場合でも、someMethodtheThing によって replaceThing のスコープ外 (たとえば、グローバル スコープ内) に渡すことができます。呼ばれる。

someMethodunused クロージャのスコープを共有しているため、含まれている originalThing への unused 参照により強制的にそれは生き続けます (2 つのクロージャ間の共有スコープ全体)。これにより、収集ができなくなります。

このコードを繰り返し実行すると、メモリ使用量が着実に増加していることがわかりますが、GC を実行してもメモリ使用量は減りません。基本的に、クロージャのリンクされたリストが実行時に作成され (そのルートは変数 theThing の形式で存在します)、各クロージャのスコープは大きな配列を間接的に参照します。これにより、かなりのメモリ リークが発生しました。

4. DOM からの参照の切り離し

DOM ノードをデータ構造に保存すると便利な場合があります。テーブル内の複数の行をすばやく更新したい場合は、各 DOM 行への参照をディクショナリまたは配列に保存できます。このように、同じ DOM 要素に対して 2 つの参照が存在します。1 つは DOM ツリー内に、もう 1 つはディクショナリ内にあります。将来のある時点でこれらの行を削除する場合は、両方の参照にアクセスできないようにする必要があります。

DOM ツリー内の内部ノードまたはリーフ ノードを参照するときに考慮すべき別の問題があります。コード内にテーブル セル (

静的メモリ割り当て 動的メモリ割り当て
サイズはコンパイル時に知る必要がある サイズはコンパイル時に知る必要はない
#ヒープに割り当て
タグ) への参照を保持し、その特定のセルへの参照を保持したまま DOM からテーブルを削除すると、メモリ リークが発生する可能性があります。

ガベージ コレクターがそのセル以外のすべてを解放すると考えるかもしれません。ただし、これは当てはまりません。セルはテーブルの子ノードであり、子ノードは親ノードへの参照を保持しているため、テーブルのセルへのこの参照は テーブル全体をメモリ内に保持します。ノードを削除するには、その子ノードを削除します。

以上がJavaScript メモリ管理の概要 + 4 つの一般的なメモリ リークへの対処方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はsegmentfault.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。