関連する推奨事項: 「JavaScript ビデオ チュートリアル 」
ほとんどの場合、私たちはメモリ管理に関する知識を理解せずに開発するだけです。 JS エンジンがこれを処理します。ただし、メモリ リークなどの問題が発生する場合があり、メモリ割り当ての仕組みを知ることによってのみ、これらの問題を解決できます。
この記事では、主に メモリ割り当て と ガベージ コレクション の動作原理と、いくつかの一般的な メモリ リーク 問題を回避する方法を紹介します。
JS では、変数、関数、またはオブジェクトを作成すると、JS エンジンがそれにメモリを割り当て、不要になったら解放します。
メモリの割り当てはメモリ内のスペースを予約するプロセスであり、メモリの解放はスペースを解放して他の目的に備えます。
変数を割り当てたり、関数を作成したりするたびに、その変数の保存は同じ段階を経ます:
メモリの割り当て
メモリの使用
メモリの解放
メモリ管理の文脈における「オブジェクト」には、JS オブジェクトだけでなく、関数や関数スコープも含まれます。
これで、JS で定義したすべてのものに対して、エンジンがメモリを割り当て、メモリが不要になったときにメモリを解放することがわかりました。
次に頭に浮かぶ疑問は、これらのものはどこに保管されるのかということです。
JS エンジンは、メモリ ヒープと スタックの 2 つの場所にデータを保存できます。ヒープとスタックは、エンジンによってさまざまな目的で使用される 2 つのデータ構造です。
スタックは、JS が静的データを保存するために使用するデータ構造です。静的データは、エンジンがコンパイル時にサイズを認識しているデータです。 JS には、オブジェクトと関数を指すプリミティブ値が含まれます (strings
、number
、boolean
、undependent
、および null
) と参照型。
エンジンはサイズが変わらないことを認識しているため、各値に固定量のメモリを割り当てます。
実行直前にメモリを割り当てるプロセスは、静的メモリ割り当てと呼ばれます。これらの値とスタック全体の制限はブラウザーに依存します。
ヒープはデータを保存するための別のスペースであり、JS はここに objects と functions を保存します。
スタックとは異なり、JS エンジンはこれらのオブジェクトに固定量のメモリを割り当てませんが、必要に応じてスペースを割り当てます。このメモリ割り当て方法は、動的メモリ割り当てとも呼ばれます。
これら 2 つのストアの特徴を以下で比較します。
スタック | ヒープ |
---|---|
ストレージの基本型と参照 サイズはコンパイル時に判明します 固定量のメモリを割り当てます |
オブジェクトと関数 サイズはコンパイル時に判明しますランタイム 制限なし |
イメージを強化するためにいくつかの例を見てみましょう。
const person = { name: 'John', age: 24, };
JS は、ヒープ内のこのオブジェクトにメモリを割り当てます。実際の値は元の値のままであるため、スタックに保存されます。
const hobbies = ['hiking', 'reading'];
配列もオブジェクトなので、ヒープに格納されます。
let name = 'John'; // 为字符串分配内存 const age = 24; // 为字分配内存 name = 'John Doe'; // 为新字符串分配内存 const firstName = name.slice(0,4); // 为新字符串分配内存
初期値は不変であるため、JS は元の値を変更せず、新しい値を作成します。
すべての変数は最初に stack を指します。非プリミティブ値の場合、 スタック
には ヒープ
内のオブジェクトへの参照が含まれます。
ヒープ メモリは特定の方法で並べ替えられていないため、スタック上にヒープ メモリへの参照を保持する必要があります。 references
をアドレス、ヒープ内のオブジェクトをそれらのアドレスが属するハウスと考えることができます。
JS は objects と functions をヒープに保存することに注意してください。プリミティブ型と参照はスタックに保存されます。
この写真では、さまざまな値がどのように保存されているかを観察できます。 person
と newperson
がどちらも同じオブジェクトを指していることに注目してください。
const person = { name: 'John', age: 24, };
これにより、ヒープ内に新しいオブジェクトが作成され、スタック上にそのオブジェクトへの参照が作成されます。
JS がさまざまなオブジェクトにメモリを割り当てる方法がわかりましたが、メモリのライフ サイクルには最後のステップが 1 つあります: メモリを解放する。
メモリ割り当てと同様に、JavaScript エンジンがこのステップも処理します。より具体的には、ガベージ コレクターがこのジョブを担当します。
JS エンジンは、変数または関数が不要になったことを認識すると、占有していたメモリを解放します。
これに関する主な問題は、一部のメモリがまだ必要かどうかが判断できないことです。つまり、不要になった瞬間にすべてのメモリを即座に収集できるアルゴリズムを使用することは不可能です。不要になりました。
一部のアルゴリズムはこの問題をうまく解決できます。このセクションでは、最も一般的な方法である 参照カウント
アルゴリズムと マーク クリアリング
アルゴリズムについて説明します。
変数が宣言され、その変数に参照型の値が割り当てられると、この値への参照の数は 1
になります。同じ値が別の変数に割り当てられている場合、その値への参照の数は 1
だけ増加します。逆に、この値への参照を含む変数が別の値を取得すると、この値への参照の数は 1
だけ減ります。
この値への参照の数が 0
になると、この値にアクセスする方法がなくなったことを意味するため、この値が占有しているメモリ領域を再利用できます。こうすることで、次回ガベージ コレクターが実行されるときに、参照がゼロの値によって占められていたメモリが解放されます。
以下の例を見てみましょう。
最後の参照がオブジェクトであるため、最後のフレームでは hobbies
だけがヒープに残ることに注意してください。
参照カウント
このアルゴリズムの問題は、循環参照が考慮されていないことです。これは、1 つ以上のオブジェクトが相互に参照しているが、コードからアクセスできなくなった場合に発生します。
let son = { name: 'John', }; let dad = { name: 'Johnson', } son.dad = dad; dad.son = son; son = null; dad = null;
親オブジェクトは相互に参照しているため、アルゴリズムは割り当てられたメモリを解放せず、両方のオブジェクトにアクセスできなくなります。
これらを null
に設定すると、それらはすべて受信参照を持っているため、参照カウント アルゴリズムはそれらが使用されなくなったことを認識しません。
マーク アンド クリア アルゴリズムには、循環依存関係に対するソリューションが含まれています。指定されたオブジェクトへの参照を単に計算するのではなく、root
オブジェクトからアクセスできるかどうかを検出します。
ブラウザの root
は window
オブジェクトですが、NodeJS の root
は global
です。
このアルゴリズムは、到達不能なオブジェクトをガベージとしてマークし、スキャン (収集) します。ルート オブジェクトは決して収集されません。
これにより、循環依存関係は問題なくなります。前の例では、dad
オブジェクトも son
オブジェクトもルートからアクセスできません。したがって、それらはすべてガベージとしてマークされ、収集されます。
このアルゴリズムは、2012 年以降、すべての最新のブラウザーに実装されています。パフォーマンスと実装のみが改善されていますが、アルゴリズムの核となる考え方は同じままです。
自動ガベージ コレクションにより、メモリ管理に時間を無駄にすることなく、アプリケーションの構築に集中できるようになります。ただし、トレードオフもあります。
アルゴリズムはメモリがいつ必要でなくなるかを正確に認識できないため、JS アプリケーションは実際に必要なメモリよりも多くのメモリを使用する可能性があります。
オブジェクトがガベージとしてマークされている場合でも、割り当てられたメモリをいつ収集するかどうかはガベージ コレクタによって決定されます。
如果你希望应用程序尽可能提高内存效率,那么最好使用低级语言。 但是请记住,这需要权衡取舍。
收集垃圾的算法通常会定期运行以清理未使用的对象。
问题是我们开发人员不知道何时会回收。 收集大量垃圾或频繁收集垃圾可能会影响性能。然而,用户或开发人员通常不会注意到这种影响。
在全局变量中存储数据,最常见内存问题可能是内存泄漏。
在浏览器的 JS 中,如果省略var
,const
或let
,则变量会被加到window
对象中。
users = getUsers();
在严格模式下可以避免这种情况。
除了意外地将变量添加到根目录之外,在许多情况下,我们需要这样来使用全局变量,但是一旦不需要时,要记得手动的把它释放了。
释放它很简单,把 null
给它就行了。
window.users = null;
忘记计时器和回调可以使我们的应用程序的内存使用量增加。 特别是在单页应用程序(SPA)中,在动态添加事件侦听器和回调时必须小心。
const object = {}; const intervalId = setInterval(function() { // 这里使用的所有东西都无法收集直到清除`setInterval` doSomething(object); }, 2000);
上面的代码每2秒运行一次该函数。 如果我们的项目中有这样的代码,很有可能不需要一直运行它。
只要setInterval
没有被取消,则其中的引用对象就不会被垃圾回收。
确保在不再需要时清除它。
clearInterval(intervalId);
假设我们向按钮添加了onclick
侦听器,之后该按钮将被删除。旧的浏览器无法收集侦听器,但是如今,这不再是问题。
不过,当我们不再需要事件侦听器时,删除它们仍然是一个好的做法。
const element = document.getElementById('button'); const onClick = () => alert('hi'); element.addEventListener('click', onClick); element.removeEventListener('click', onClick); element.parentNode.removeChild(element);
内存泄漏与前面的内存泄漏类似:它发生在用 JS 存储DOM
元素时。
const elements = []; const element = document.getElementById('button'); elements.push(element); function removeAllElements() { elements.forEach((item) => { document.body.removeChild(document.getElementById(item.id)) }); }
删除这些元素时,我们还需要确保也从数组中删除该元素。否则,将无法收集这些DOM元素。
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); }); }
由于每个DOM元素也保留对其父节点的引用,因此可以防止垃圾收集器收集元素的父元素和子元素。
在本文中,我们总结了 JS 中内存管理的核心概念。写这篇文章可以帮助我们理清一些我们不完全理解的概念。
希望这篇对你有所帮助,我们下期再见,记得三连哦!
原文地址:https://felixgerschau.com/javascript-memory-management/
作者:Ahmad shaded
译文地址:https://segmentfault.com/a/1190000037651993
更多编程相关知识,请访问:编程入门!!
以上がJavaScriptのメモリ管理を詳しく解説の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。