js メモリ リークのシナリオ、それらを詳細に監視および分析する方法

js メモリ リークのシナリオ、それらを詳細に監視および分析する方法

序文

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

文字通り、要求されたメモリは時間内に回復されず、リークされました。

Q: メモリリークはなぜ発生するのでしょうか?

フロントエンドにガベージ コレクション メカニズムがありますが、ガベージ コレクション メカニズムによって無駄なメモリがガベージとして処理されない場合、メモリ リークが発生します。

ガベージ コレクション メカニズムでは、通常、フラグをクリアする戦略が使用されます。簡単に言えば、ルート ノードから到達可能かどうかを参照して、ガベージかどうかを判断します。

上記がメモリリークの根本的な原因です。直接的な原因は、ライフサイクルが異なる 2 つのものが通信する際に、一方が期限切れになりリサイクルされるべきなのに、もう一方がまだ保持しているためにメモリリークが発生することです。

それでは、どのようなシナリオでメモリリークが発生するのかについて話しましょう。

どのような状況でメモリリークが発生する可能性がありますか?

1. 偶発的なグローバル変数

グローバル変数のライフ サイクルは最も長いです。ページが閉じられるまで存続するため、グローバル変数のメモリがリサイクルされることはありません。

メモリ リークは、グローバル変数が不適切に使用されたり、時間内にリサイクルされなかったり (手動で null が割り当てられたり)、スペル エラーによりグローバル変数にマウントされたりした場合に発生します。

2. タイマーを忘れた

setTimeout と setInterval のライフ サイクルはブラウザーの専用スレッドによって維持されるため、ページでタイマーが使用されている場合、ページが破棄されるときにタイマーが手動で解放およびクリーンアップされなければ、タイマーは存続したままになります。

つまり、タイマーのライフサイクルはページに紐付けられていないため、現在のページの js にタイマーを介してコールバック関数が登録され、そのコールバック関数が現在のページの変数または一部の DOM 要素を保持している場合、ページが破壊されてしまいます。タイマーが参照しているページ部分は正常にリサイクルできず、メモリリークが発生します。

このとき、同じページを再度開くと、実際にはメモリ内にページデータのコピーが2つ存在します。何度も閉じて開くと、メモリリークはますます深刻になります。タイマーを使用する人はクリアを忘れやすいため、このシナリオは発生しやすいです。

3. クロージャの不適切な使用

関数自体は、それが定義されている語彙環境への参照を保持しますが、通常、関数が使用された後、関数によって要求されたメモリは再利用されます。

ただし、関数内から関数が返されると、返された関数は外部関数の語彙環境を保持し、返された関数は他のライフサイクル オブジェクトによって保持されるため、外部関数は実行されますが、メモリを再利用することはできません。

したがって、返される関数のライフサイクルは長すぎてはならず、クロージャが時間内にリサイクルされる必要があります。

通常、クロージャはメモリ リークを引き起こしません。これは、外部関数のレキシカル環境を保持することがクロージャの機能であり、このメモリがリサイクルされるのを防ぐためです。

将来必要になる可能性もありますが、メモリ消費の原因となることは間違いないので、過度に使用することはお勧めできません。

4. DOM要素が見つからない

DOM 要素のライフ サイクルは通常、DOM ツリーにマウントされているかどうかによって決まります。DOM ツリーから削除されると、破棄されてリサイクルされます。

ただし、DOM 要素が js でも参照されている場合、そのライフサイクルは js と、DOM ツリー上にあるかどうかの両方によって決まります。削除する場合、正常にリサイクルするには両方の場所をクリーンアップする必要があることに注意してください。

5. ネットワークコールバック

いくつかのシナリオでは、ページでネットワーク リクエストが開始され、コールバックが登録され、コールバック関数がページの一部のコンテンツを保持します。その後、ページが破棄されると、ネットワーク コールバックを登録解除する必要があります。そうしないと、ネットワークがページ コンテンツの一部を保持しているため、ページ コンテンツの一部をリサイクルできません。

メモリリークを監視する方法

メモリ リークは 2 つのカテゴリに分けられます。1 つはより深刻で、リークしたメモリは回復できません。もう 1 つはそれほど深刻ではなく、メモリ リークが時間内にクリーンアップされないことで発生します。一定期間後にはクリーンアップできます。

どちらにしても、開発者ツールでキャプチャされたメモリ グラフを使用すると、メモリ使用量が一定期間にわたって直線的に減少し続けることがわかります。これは、GC (ガベージ コレクション) が継続的に発生しているためです。

より深刻な最初のタイプでは、メモリ グラフで GC が継続的に発生した後でも、使用されるメモリの合計量が依然として増加していることがわかります。

また、メモリが不足するとGCが継続的に発生し、GCによってメインスレッドがブロックされ、ページのパフォーマンスに影響を及ぼし、ジャムが発生するため、メモリリークには依然として注意を払う必要があります。

このようなシナリオを想定して、開発者ツールを使用してメモリ リークをチェックしてみましょう。

シナリオ 1: 関数内でメモリ ブロックが要求され、その後関数が短時間に繰り返し呼び出される

// ボタンをクリックすると関数が1回実行され、メモリの一部が適用されます startBtn.addEventListener("click", function() {
	var a = 新しい配列(100000).fill(1);
	var b = 新しい配列(20000).fill(1);
});

ページが使用できるメモリには制限があります。メモリが不足すると、ガベージ コレクション メカニズムがトリガーされ、未使用のメモリがリサイクルされます。

関数内で使用される変数はすべてローカル変数です。関数の実行後、このメモリは不要になり、リサイクルできます。

そのため、この関数を短期間に繰り返し呼び出すと、関数の実行時にメモリが不足していることが判明し、ガベージ コレクション メカニズムが動作して、前の関数によって要求されたメモリをリサイクルします。前の関数が実行されているため、メモリは不要であり、リサイクルできます。

したがって、メモリ使用量を示すグラフは、中央に複数の垂直線がある水平線になります。これは、実際にはメモリがクリアされ、再割り当てされ、クリアされて再割り当てされていることを意味します。各垂直線の位置は、ガベージ コレクション メカニズムが動作し、関数が実行されて再割り当てされる時間です。

シナリオ2: 関数内でメモリブロックが要求され、その後関数が短時間に繰り返し呼び出されるが、そのたびに要求されるメモリの一部は外部に保持される。

// ボタンをクリックして関数を 1 回実行し、メモリの一部を適用します。var arr = [];
startBtn.addEventListener("クリック", 関数() {
	var a = 新しい配列(100000).fill(1);
	var b = 新しい配列(20000).fill(1);
    arr.push(b);
});

最初の写真との違いは何ですか?

もはや水平線ではなく、水平線内の各垂直線の底部は同じ高さではありません。

これは実際にはメモリリークです。

関数内で 2 つの配列メモリを適用しますが、配列の 1 つは外部に保持されます。すると、各関数が実行された後でも、外部に保持された配列メモリのこの部分は回復できず、毎回メモリの一部しか回復できません。

このように、関数呼び出しの数が増えると、より多くのメモリを回復できず、メモリリークが発生し、メモリ使用量が増加し続けます。また、パフォーマンスモニターツールを使用することもできます。開発者ツールの[詳細]ボタンを見つけて、この機能パネルを開きます。これは、CPU、メモリなどの使用状況をリアルタイムで監視できるツールです。一定期間しかキャプチャできない上記のツールよりも直感的です。

はしご状の上昇はメモリ リークを示しています。関数が呼び出されるたびに、一部のデータが外部に保持され、再利用できません。滑らかな上昇は、使用後にデータが正常に再利用できることを示しています。

最初の赤いボックスの端に直線があることに注目してください。これは、コードを変更し、外部保持関数の配列に適用されたコード行を削除してから、ページを更新し、手動で GC をクリックして効果をトリガーしたためです。そうしないと、GC をどのようにクリックしても、一部のメモリを回復できず、この効果は得られません。

上記はメモリ リークの発生を監視するためのツールですが、次のステップが鍵となります。メモリ リークが見つかったら、どうやってその場所を特定するのでしょうか。データのどの部分が回復されずに漏洩の原因となったのかをどうやって知るのでしょうか?

メモリリークを分析して問題のあるコードを見つける方法

メモリ リークの原因を分析するには、開発者ツールのメモリ機能を使用する必要があります。この機能を使用すると、メモリ スナップショット、一定期間内のメモリ割り当て、一定期間内のメモリ割り当てをトリガーする各関数をキャプチャできます。

これらのツールを使用すると、特定の瞬間にどの関数操作がメモリ割り当てを引き起こしたかを分析し、繰り返されてリサイクルされないオブジェクトが何であるかを分析できます。

このようにして、疑わしい関数とオブジェクトが判明し、その後コードを分析して、その関数内のオブジェクトがメモリ リークの原因であるかどうかを確認します。

まず簡単な例を挙げて、次に実際のメモリ リークの例を挙げてみましょう。

シナリオ1: 関数内でメモリブロックが要求され、その後関数が短時間に繰り返し呼び出されるが、そのたびに要求されるメモリの一部は外部に保持される。

// ボタンをクリックするたびに、一部のメモリは外部によって保持されるため回復できません。arr var arr = [];
startBtn.addEventListener("クリック", 関数() {
	var a = 新しい配列(100000).fill(1);
	var b = 新しい配列(20000).fill(1);
    arr.push(b);
});

メモリスナップショット

上の図に示すように、2 つのスナップショットをキャプチャし、2 つのスナップショット間でメモリ リーク操作を実行し、最後に 2 つのスナップショット間の違いを比較して、どのオブジェクトが追加され、どのオブジェクトがリサイクルされたかを確認できます。

また、次の図に示すように、特定の時間のスナップショットを表示し、メモリ使用率からどのオブジェクトが大量のメモリを占有しているかを確認することもできます。

ガベージ コレクション メカニズムを使用して、GC ルート ノードから到達可能なオブジェクトの中で、どのオブジェクトが大量のメモリを占有しているかを確認することもできます。

上記の方法から始めて、現在どのオブジェクトが大量のメモリを占有しているかを確認できます。一般的に言えば、これが疑わしいものです。

もちろん、必ずしもそうとは限りません。疑わしいオブジェクトがある場合、複数のメモリスナップショットを使用して比較し、途中で手動でGCを強制し、回収されたオブジェクトが回収されたかどうかを確認することができます。これは1つのアイデアです。

一定期間内のメモリ割り当てをキャプチャする

この方法では、各メモリ割り当ての瞬間にどの関数が開始したか、およびメモリに格納されているオブジェクトを選択的に表示できます。

もちろん、メモリ割り当ては正常な動作です。ここで表示される内容は、オブジェクトが疑わしいかどうかを判断するために、メモリ使用率やメモリ スナップショットとの組み合わせなど、他のデータも必要です。

一定期間にわたる関数のメモリ使用量をキャプチャする

表示される内容は非常に少なく、比較的単純で、明確な目的、つまり、どの操作がメモリに適用されているか、一定期間内にどれだけのメモリが使用されているかを示します。

つまり、これらのツールは直接的な答えを提供できず、xxx がメモリ リークの原因であると伝えることはできません。ブラウザ レベルで特定できる場合、なぜそれをリサイクルしてメモリ リークを引き起こさないのでしょうか。

したがって、これらのツールはさまざまなメモリ使用量情報しか提供できません。この情報を使用して、独自のコードのロジックに従って、どの疑わしいオブジェクトがメモリ リークの原因であるかを分析する必要があります。

分析例

以下は、インターネット上の多くの記事に登場したメモリ リークの例です。

var t = null;
var replaceThing = 関数() {
  var o = t
  var 未使用 = 関数() {
    もし(o){
      console.log("こんにちは")
    }        
  }
 
  t = {
        longStr: 新しい配列(100000).fill('*'),
        いくつかのメソッド: 関数() {
                       コンソール.log(1)
                    }
      }
}
間隔を設定します(replaceThing, 1000)

このコードがメモリ リークを引き起こすかどうかまだわからないかもしれません。心配しないでください。

まず、このコードの目的についてお話ししましょう。グローバル変数 t と replaceThing 関数を宣言します。関数の目的は、グローバル変数に新しいオブジェクトを割り当てることです。次に、置換される前にグローバル変数 t の値を格納するための内部変数があります。最後に、タイマーが replaceThing 関数を定期的に実行します。

  • 問題を見つける

ツールを使用して、メモリ リークがあるかどうかを確認してみましょう。

3 つのメモリ監視チャートはすべて、メモリ リークが発生していることを示しています。同じ関数を繰り返し実行するとメモリがラダー状に増加し、GC を手動でクリックしてもメモリは減少しません。これは、関数が実行されるたびにメモリ リークが発生していることを示しています。

手動で強制的にガベージ コレクションを実行してもメモリを削減できない状況は非常に深刻です。この状態が長時間続くと、使用可能なメモリが枯渇し、ページがフリーズしたり、クラッシュしたりします。

  • 問題を分析する

メモリ リークが発生していることが判明したので、次のステップはメモリ リークの原因を突き止めることです。

まず、サンプリング プロファイルを通じて、replaceThing 関数で容疑者を特定します。

次に、2 つのメモリ スナップショットを取得し、それらを比較して、情報を取得できるかどうかを確認します。

2 つのスナップショットを比較すると、このプロセス中に配列オブジェクトが増加していることがわかります。この配列オブジェクトは、replaceThing 関数内で作成されたオブジェクトの longStr 属性から取得されます。

実際、この図には多くの情報が含まれていますが、特に下のネストされた図には多くの情報が含まれています。ネストされた関係は逆になっています。逆から見ると、グローバル オブジェクト Window が配列オブジェクトに段階的にアクセスする方法がわかります。このような到達可能なアクセス パスがあるため、ガベージ コレクション メカニズムはリサイクルできません。

実はここで分析することができます。より多くのツールを使用するために、画像を変更して分析してみましょう。

2 番目のメモリ スナップショットから直接開始して確認してみましょう。

最初のスナップショットから 2 番目のスナップショットまで、replaceThing が 7 回実行され、正確に 7 個のオブジェクトが作成されました。これらのオブジェクトはリサイクルされていないようです。

ではなぜリサイクルされないのでしょうか?

replaceThing 関数は以前のオブジェクトを内部的に保存するだけですが、関数が終了したら、ローカル変数はリサイクルされるべきではないでしょうか?

図を引き続き見ていくと、大量のメモリを消費するクロージャが下に存在することがわかります。

replaceThing 関数が呼び出されるたびに、内部で作成されたオブジェクトをリサイクルできないのはなぜですか?

replaceThingは初めて作成されるため、このオブジェクトはグローバル変数tによって保持され、再利用できません。

後続の各呼び出しでは、このオブジェクトは前の replaceThing 関数内の o ローカル変数によって保持され、回復できません。

この関数内のローカル変数 o は、replaceThing が初めて呼び出されたときに作成されたオブジェクトの someMethod メソッドによって保持されます。このメソッドによってマウントされたオブジェクトは、グローバル変数 t によって保持されるため、回復できません。

このように、関数が呼び出されるたびに、関数が最後に呼び出されたときに内部的に作成されたローカル変数が保持され、関数が実行された後でもこれらのローカル変数を回復できなくなります。

言葉で説明すると少しわかりにくいですが、ここに図があります (権利を侵害している場合は削除します)。ガベージ コレクション メカニズムのマーク アンド スイープ方式 (一般に到達可能性方式として知られています) と組み合わせると、非常に明確になります。

  • 結論

メモリ分析ツールを使用すると、次の情報を取得できます。

  1. 同じ関数呼び出しのメモリ使用量がラダーのように増加し、手動 GC メモリを削減できないため、メモリ リークが発生していることがわかります。
  2. 一定期間のメモリ使用量をキャプチャすることで、疑わしい関数がreplaceThingであると判断できます。
  3. メモリ スナップショットを比較すると、リサイクルされなかったオブジェクトは replaceThing 内で作成されたオブジェクト (ストレージ配列の longStr プロパティとメソッド someMethod を含む) であることがわかりました。
  4. メモリ スナップショットをさらに分析すると、リサイクルされない理由は、各関数呼び出しによって作成されたオブジェクトが、前の関数呼び出し中に内部的に作成されたローカル変数 o に格納されるためであることが明らかになりました。
  5. ローカル変数 o は、作成されたオブジェクトの someMethod メソッドによって保持されるため、関数実行の最後にはリサイクルされません。

上記が結論ですが、なぜこのようなことが起こるのかを分析する必要がありますね。

実際、これには閉鎖の知識ポイントが関係します。

MDN では、クロージャを関数ブロックと関数が定義されている語彙環境の組み合わせとして説明しています。

関数が定義されると、現在の語彙環境を格納するスコープの内部属性が存在します。そのため、関数が、その語彙環境よりも長いライフサイクルを持つ何かによって保持されると、関数によって保持される語彙環境はリサイクルできなくなります。

簡単に言えば、関数内に外部関数が定義されている場合、内部関数が外部関数の一部の変数を使用すると、これらの変数は内部関数のプロパティに格納されるため、外部関数が実行されても再利用できません。

もう一つの知識ポイントは、外部関数で定義されたすべての関数がクロージャを共有することです。つまり、関数bは外部関数の変数を使用しますが、関数cはそれを使用しなくても、関数cは変数aを格納します。これは共有クロージャと呼ばれます。

この質問に戻る

replaceThing 関数では、内部で作成されたリテラル オブジェクトが手動でグローバル変数に割り当てられ、このオブジェクトにも someMethod メソッドがあるため、クロージャ機能により someMethod メソッドが replaceThing 変数を格納します。

someMethod はローカル変数を使用しませんが、replaceThing 内に未使用の関数があります。この関数はローカル変数 o を使用します。共有クロージャのため、someMethod も o を格納します。

また、o は置き換えられる前のグローバル変数 t の値も保存するため、関数が呼び出されるたびに誰かが内部変数 o を保持してしまい、再利用できません。このメモリ リークを解決するには、o の保持者を切り離して、ローカル変数 o が正常に再利用できるようにする必要があります。

したがって、2 つのアイデアがあります。someMethod に o を保存しないようにするか、使用後に o を解放するかのいずれかです。

未使用の関数が役に立たない場合は、関数を直接削除してその効果を確認できます。

ここでメモリが梯子状に上昇しているのは、現在のメモリがまだ十分であり、ガベージ コレクション メカニズムがトリガーされていないためです。手動で GC をトリガーするか、しばらく実行して GC が機能するまで待って、メモリが初期状態まで低下するかどうかを確認できます。これは、すべてのメモリをリサイクルできることを示しています。

または、メモリ スナップショットを取得して、スナップショットを取得するときに、スナップショットを取得する前に GC が自動的に実行されることを確認することもできます。

そうですか? replaceThing 関数が定期的に呼び出されても、関数内のローカル変数 o には以前のグローバル変数 t の値が格納されますが、結局のところローカル変数です。関数が実行された後、外部参照がない場合はリサイクルできるため、最終的にはグローバル変数 t に格納されたオブジェクトのみがメモリに残ります。もちろん、使用していない関数を削除できない場合は、使用後に o 変数を手動で解放することを覚えておくしかありません。

var 未使用 = 関数() {
    もし(o){
      console.log("こんにちは")
      o = ヌル;
    }        
}

しかし、このアプローチでは根本的な原因は解決されません。未使用の関数が実行される前に、このメモリの山はまだ存在し、リークしたままで回復できないからです。最初との違いは、少なくとも未使用の関数が実行された後は、解放できるということです。

実際、ここでのコードに問題があるかどうか、なぜローカル変数が保存に必要なのか、なぜ未使用の関数が必要なのか、そしてこの関数の目的は何なのかを検討する必要があります。以前のグローバル変数 t が将来のある時点で使用可能かどうかを判断するだけであれば、別のグローバル変数を使用して保存しないのはなぜでしょうか。なぜローカル変数を選択するのでしょうか。

したがって、コードを書くときは、クロージャが関係するシナリオに特に注意する必要があります。不適切に使用すると、深刻なメモリ リークが発生する可能性があります。クロージャを使用すると、関数が外部のレキシカル環境を保持できるため、外部のレキシカル環境の一部の変数をリサイクルできなくなること、およびクロージャを共有する機能があることを覚えておく必要があります。この 2 つの点を理解して初めて、深刻なメモリ リークを回避するための使用シナリオに関して、クロージャの実装方法を正しく検討できます。

要約する

これで、js メモリ リーク シナリオとその監視および分析方法に関するこの記事は終了です。js メモリ リーク シナリオ監視の詳細については、123WORDPRESS.COM の以前の記事を検索するか、次の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • 一般的な JS メモリ リークとその解決策の分析
  • JavaScript プログラムにおけるメモリリークの詳細な理解
  • JavaScript メモリリークの詳細な説明
  • JS によるメモリリークの例をいくつか分析する
  • JavaScript のメモリリークに対処する方法
  • JavaScript のメモリ リークに関する入門とチュートリアル (推奨)
  • JavaScript のメモリリークを理解するための記事

<<:  aタグのname属性とid属性を使用してページ内を移動する方法

>>:  K3s 入門ガイド - Docker で K3s を実行するための詳細なチュートリアル

推薦する

VueはEchartsを使用して3次元棒グラフを実装します

この記事では、Echartsを使用して3次元棒グラフを実装するVueの具体的なコードを参考までに共有...

生年月日を年齢に変換し、グループ化して人数を数えるMySQLの例

データベースのクエリ `学生`から*を選択 クエリ結果id名前誕生日1張三1970-10-01 2李...

UbuntuはSSHサービスのリモートログイン操作を開始します

ssh-secure シェルは、安全なリモート ログインを提供します。組み込みシステムを開発し、Li...

Vue3はフロントエンドのログを出力するためにaxiosインターセプターを使用する

目次1. はじめに2. axiosインターセプターを使用してフロントエンドログを出力する1. はじめ...

Ubuntuが仮想マシンでインターネットに接続できない問題の解決策

インターネットに接続できない仮想マシンをセットアップするのは非常に面倒です。ここでは、Ubuntu ...

MySQLパスワード変更例の詳細な説明

MySQLパスワード変更例の詳細な説明長い間 MySQL を使用していませんでした。今日、MySQL...

Flex レイアウトで適応型ページを作成する (構文と例)

Flex レイアウトの紹介英語の Flex はフレキシブル ボックス、つまり伸縮性のあるボックスを...

Ubuntu 20.04 に GitLab をインストールして設定する方法

導入GitLab CE または Community Edition は、主に Git リポジトリのホ...

Linux での一般的なシェル スクリプト コマンドと関連知識

目次1. 覚えておくべき知識1. 変数タイプ2. シェル変数の説明3. シングルクォート、ダブルクォ...

Centos7 で yum を使用して Mysql5.7.19 をインストールする詳細な手順

Centos7 の yum ソースには、mysql の代わりに mariaDB が使用されているため...

Angularが予期しない例外エラーを処理する方法の詳細な説明

前面に書かれたコードがどれだけ適切に記述されていても、すべての可能性のある例外を完全に処理することは...

MySQLのint主キーの自己増分の問題を解決する

導入MySQL データベースを使用する場合、int を主キーとして使用し、自動インクリメントに設定す...

CD コマンドを使わずに Linux でディレクトリ/フォルダに入る方法

ご存知のとおり、cd コマンドがないと、Linux でディレクトリを切り替えることはできません。それ...

クロスブラウザ開発体験のまとめ(I)HTMLタグ

ページにDOCTYPEを追加するブラウザによってタグやスタイルシートの解釈が異なるため、さまざまなブ...

NavicatでMySQLビッグデータをインポートする際のエラーの解決方法

Navicat がエクスポートしたデータはインポートできません。最後に、MySQLコマンドのインポー...