JavaScriptメモリ管理の基礎:それがどのように機能し、どのような問題が発生する可能性があるか





ほとんどの開発者は、JavaScriptのメモリ管理がどのように実装されているかについてほとんど考えません。エンジンは通常、プログラマーのためにすべてを行うため、プログラマーがメモリ管理メカニズムの原則について考えることは意味がありません。



しかし遅かれ早かれ、開発者はリークなどのメモリの問題に対処する必要があります。さて、メモリ割り当てメカニズムを理解している場合にのみ、それらに対処することが可能になります。この記事は説明に専念しています。また、JavaScriptで最も一般的なタイプのメモリリークに関するヒントも提供します。



メモリライフサイクル



関数や変数などを作成するとき。JavaScriptでは、エンジンは一定量のメモリを割り当てます。次に、メモリが不要になった後、彼はそれを解放します。



実際には、メモリ割り当ては、特定の量のメモリを予約するプロセスと呼ぶことができます。まあ、その解放はシステムへの予備の返還です。何度でも再利用できます。



変数が宣言されるか、関数が作成されると、メモリは次のループを通過します。







ここにブロックで:



  • Allocateは、エンジンが実行するメモリ割り当てです。作成されたオブジェクトに必要なメモリを割り当てます。
  • 使用-メモリ使用量。開発者はこの瞬間に責任があり、メモリに読み書きするためのコードを書き込みます。
  • リリース-メモリを解放します。ここでJavaScriptが再び登場します。リザーブが解放された後、メモリは他の目的にも使用できます。


メモリ管理のコンテキストでの「オブジェクト」とは、JSオブジェクトだけでなく、関数とスコープも意味します。



メモリスタックとヒープ



一般に、すべてが明確に見えます。JavaScriptは、開発者がコードで指定するすべてのものにメモリを割り当て、すべての操作が完了すると、メモリが解放されます。しかし、データはどこに保存されていますか?



メモリスタックとヒープの2つのオプションがあります。最初のものは何ですか、2番目のものは何ですか-さまざまな目的のためにエンジンによって使用されるデータ構造の名前。



スタックは静的メモリ割り当てです スタック







の定義は多くの人に知られています。これは静的データを格納するために使用されるデータ構造であり、そのサイズはコンパイル時に常に認識されます。 JSには、文字列、数値、ブール値、未定義、nullなどのプリミティブ値、および関数とオブジェクトの参照が含まれています。



エンジンは、データのサイズが変更されないことを「理解」するため、値ごとに一定量のメモリを割り当てます。実行前にメモリを割り当てるプロセスは、静的メモリ割り当てと呼ばれます。



ブラウザはデータ型ごとに事前にメモリを割り当てるため、そこに配置できるデータのサイズには制限があります。ブラウザはデータ型ごとに事前にメモリを割り当てるため、そこに配置できるデータのサイズには制限があります。



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



ヒープに関しては、スタックと同じくらい多くの人に馴染みがあります。オブジェクトと関数を格納するために使用されます。



ただし、スタックとは異なり、エンジンは特定のオブジェクトに必要なメモリ量を「知る」ことができないため、必要に応じてメモリが割り当てられます。そして、このメモリ割り当て方法は「動的」(動的メモリ割り当て)と呼ばれます。



いくつかの例



コードへのコメントは、メモリ割り当ての微妙な違いを示しています。



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





// JavaScriptは、ヒープ上のこのオブジェクトにメモリを割り当てます。

//値自体はプリミティブであるため、スタックに格納されます。



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





//配列もオブジェクトであるため、ヒープに移動します。



名前= 'ジョン'; //文字列にメモリを割り当てます

constage = 24; //番号にメモリを割り当てます

name = 'John Doe'; //新しい行にメモリを割り当てます

constfirstName = name.slice(0,4); //改行にメモリを割り当てます



//プリミティブ値は本質的に不変です:初期値を変更する代わりに、

// JavaScriptは別の値を 作成します。



JavaScriptリンク



スタックに関しては、すべての変数がそれを指しています。値がプリミティブでない場合、スタックにはヒープオブジェクトへの参照が含まれます。



特別な順序はありません。つまり、目的のメモリ領域への参照がスタックに格納されます。このような状況では、ヒープ内のオブジェクトは建物のように見えますが、リンクはそのアドレスです。



JSは、オブジェクトと関数をヒープに格納します。しかし、プリミティブ値と参照はスタック上にあります。







この画像は、さまざまな値のストレージ構成を示しています。ここでは、personとnewPersonが同じオブジェクトを指していることに注意してください。







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





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



一般に、JavaScriptではリンクが非常に重要です。



ガベージコレクション



今こそ、記憶のライフサイクル、つまりその解放に戻る時です。



JavaScriptエンジンは、メモリの割り当てだけでなく、割り当て解除も担当します。この場合、ガベージコレクタはメモリをシステムに返します。



そして、エンジンが変数または関数が不要になったことを「認識する」とすぐに、メモリが解放されます。



しかし、ここには重要な問題があります。事実は、メモリの特定の領域が必要かどうかを判断することは不可能です。リアルタイムでメモリを解放するほど正確なアルゴリズムはありません。



確かに、これを可能にするうまく機能するアルゴリズムがあります。それらは完璧ではありませんが、それでも他の多くのものよりはるかに優れています。以下-参照カウントに基づくガベージコレクションと「フラグ付けアルゴリズム」についての話。



リンクはどうですか?



これは非常に単純なアルゴリズムです。他の参照ポイントがないオブジェクトを削除します。 これはそれをかなりよく説明する例です。



ビデオを見たことがあれば、スタックで参照されているヒープ内のオブジェクトは趣味だけであることに気付いたと思います。



サイクル



アルゴリズムの欠点は、循環参照を考慮に入れることができないことです。これらは、コードの観点からは手の届かないところにある、1つ以上のオブジェクトが相互に参照している場合に発生します。



let son = {
  name: 'John',
};
let dad = {
  name: 'Johnson',
}
 
son.dad = dad;
dad.son = son;
son = null;
dad = null;
      
      







ここで息子とお父さんはお互いを参照しています。長い間オブジェクトにアクセスすることはできませんが、アルゴリズムはオブジェクトに割り当てられたメモリを解放しません。



アルゴリズムが参照をカウントするため、すべてのオブジェクトにはまだ参照があるため、オブジェクトにnullを割り当てても何も起こりません。



注釈のアルゴリズム



ここで、マークアンドスイープ法と呼ばれる別のアルゴリズムが役に立ちます。このアルゴリズムは参照をカウントしませんが、ルートオブジェクトを介してさまざまなオブジェクトにアクセスできるかどうかを決定します。ブラウザでは、これはウィンドウであり、Node.jsではグローバルです。







オブジェクトが使用できない場合、アルゴリズムはオブジェクトにマークを付けてから削除します。この場合、ルートオブジェクトが破棄されることはありません。循環参照の問題はここでは関係ありません。アルゴリズムにより、お父さんも息子もすでにアクセスできないことを理解できるため、それらを「一掃」してメモリをシステムに戻すことができます。



2012年以降、すべてのブラウザーには、マークアンドスイープ方式に従って正確に機能するガベージコレクターが装備されています。



ここに欠点がないわけではありません。





すべてが順調であると考える人もいるかもしれませんが、今ではすべてをアルゴリズムに任せて、メモリ管理を忘れることができます。しかし、そうではありません。



大量のメモリ使用量



アルゴリズムはメモリが不要になった時期を認識しないため、JavaScriptアプリケーションは必要以上のメモリを使用する可能性があります。そして、割り当てられたメモリを解放するかどうかを決定できるのはコレクターだけです。



JavaScriptは、低水準言語でのメモリ管理に優れています。ただし、ここには欠点もあり、注意する必要があります。特に、JSは、プログラマーが「手動で」メモリの割り当てと解放を処理する低水準言語とは異なり、メモリ管理ツールを提供していません。



パフォーマンス



メモリは、新しい瞬間ごとにクリアされるわけではありません。リリースは定期的に実行されます。しかし、開発者はこれらのプロセスがいつ開始されるかを正確に知ることはできません。



したがって、アルゴリズムが機能するには特定のリソースが必要なため、ガベージコレクションがパフォーマンスに悪影響を与える場合があります。確かに、状況が完全に管理不能になることはめったにありません。ほとんどの場合、これの結果は微視的です。



メモリリーク



メモリリークは、開発において最も苛立たしいものの1つです。しかし、最も一般的なタイプのリークをすべて知っている場合は、問題を簡単に回避できます。



グローバル変数



メモリリークは、グローバル変数にデータが格納されていることが原因で最も頻繁に発生します。



ブラウザで、間違えてconstまたはletの代わりにvarを使用すると、エンジンは変数をウィンドウオブジェクトにアタッチします。同様に、functionという単語で定義された関数に対して操作を実行します。



user = getUser();
var secondUser = getUser();
function getUser() {
  return 'user';
}
      
      





// 3つの変数(user、secondUser、および

// getUser)はすべてウィンドウオブジェクトにアタッチされます。



これは、グローバルスコープで宣言されている関数と変数でのみ実行できます。 strictモードでコードを実行することにより、この問題を回避できます。



グローバル変数は意図的に宣言されることがよくありますが、これは必ずしも間違いではありません。ただし、いずれの場合も、データが不要になった後でメモリを解放することを忘れてはなりません。これを行うには、グローバル変数にnullを割り当てる必要があります。



window.users = null;



コールバックとタイマー



タイマーとコールバックを忘れたとしても、アプリケーションは必要以上のメモリを使用します。主な問題は、シングルページアプリケーション(SPA)と、コールバックとイベントハンドラーの動的な追加です。



忘れられたタイマー



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





この関数は2秒ごとに実行されます。その実装は無限であってはなりません。問題は、間隔に参照があるオブジェクトは、間隔がクリアされるまで破棄されないことです。したがって、タイムリーに処方する必要があります

。clearInterval(intervalId);



コールバックを忘れ



たonClickハンドラーがボタンにアタッチされていて、ボタン自体が削除された場合、問題が発生する可能性があります。たとえば、ボタンは不要になりました。



以前は、ほとんどのブラウザは、そのようなイベントハンドラに割り当てられたメモリを解放できませんでした。現在、この問題は過去のものですが、それでも、不要になったハンドラーを残すことは価値がありません。



const element = document.getElementById('button');
const onClick = () => alert('hi');
element.addEventListener('click', onClick);
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
      
      







変数内の忘れられたDOM要素



これは前のケースと同様です。このエラーは、DOM要素が変数に格納されている場合に発生します。



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);
 });
}
      
      





配列から要素を削除することにより、ページ上の要素のリストでそのコンテンツを更新します。



各house要素にはその親への参照があるため、これにより、ガベージコレクターが親によって占有されているメモリを解放して、リークが発生するのを防ぐことができます。



乾燥残留物中



この記事では、メモリ割り当ての一般的な仕組みについて説明し、作成者は、発生する可能性のある問題とその対処方法を示しました。これはすべて、Javaスクリプト開発者にとって重要です。



, Frontend- Skillbox:



, - ( SPA — Single Page Applications).



, “ ” — ( , ), , , .



— . .



, ( , , ). , , “” — garbage collector.



- (js , garbage collector’a). , .



All Articles