blog

メモリー・リーク?あなたはやられたことがある?

ガベージコレクタは、定期的にルートから開始し、ルートから参照されているオブジェクトをすべて見つけ、そのオブジェクトから参照されているオブジェクトを見つけます。その他のオブジェクトは、ガベージコレクショ...

Feb 14, 2020 · 7 min. read
シェア

スタックメモリー&

  • スタック・メモリは、未定義、NULL、文字列、数値、ブーリアンなどの基本的な型に使用されます。
  • ヒープ・メモリは、オブジェクト、配列、関数などの参照型を格納するために使用されます。

現在の実行コンテキストが終了すると、関数は破棄され、関数内で生成された基本型の変数は、クロージャなどで変数がメモリ上に残らない限り、スタックメモリ上で破棄され、ヒープメモリ上の参照型は、破棄されるかどうかを判断するためにガベージコレクションされます。

ゴミ収集メカニズム:

  1. マーカー除去方法
    • ガベージコレクタは、定期的にルートから開始し、ルートから参照されているオブジェクトをすべて見つけ、そのオブジェクトから参照されているオブジェクトを見つけ......ルートから開始し、取得可能なオブジェクトをすべて見つけ、取得できないオブジェクトをすべて収集し、まだ参照されているものは保持が必要なものとしてマークされ、その他はガベージコレクション開始時にクリアされます。残りは、ガベージコレクションが開始されたときに削除され、対応するメモリ空間が解放されます。
    • クリアできない循環参照の問題の解決
    • すべての主要なブラウザはこのメカニズムを使って、保持する必要があるものとクリアする必要があるものを決定します。
  2. 参照カウント
    • この方法はもう使われていません。
    • 原理は、各オブジェクトが参照された回数を追跡することです、同じ参照型データが2つの変数で参照されている場合、参照は2としてカウントされます、これらの2つの変数に別の値が割り当てられている場合、それは元の参照型データが0としてカウントされることを意味し、ごみ収集が開始されたときに破棄されます。
    • 欠点:循環参照は、参照カウントが0になることはありません、コンテキスト環境が終了した場合でも、まだメモリに残っている、メモリリークの原因となります。

V8ゴミ収集戦略

異なるゴミ収集戦略は、撤去のためにマークされたゴミ収集メカニズムを使用世代別収集戦略:新世代 旧世代

  1. 新しい世代のアルゴリズム:Scavengeアルゴリズムは、ヒープメモリを2つに分割され、1つはFromスペースと呼ばれる使用中であり、1つはToスペースと呼ばれる空き領域であり、その後、生きているオブジェクトがToスペースにコピーされるたびに、メモリ空間を解放するために破壊された不要な参照のFromスペースに残され、その後、FromとToのスワップの役割は、Fromスペースに元のToスペースになります。生き残ったオブジェクトのふるい分けを開始する、というように。

  2. 旧世代のアルゴリズム:Mark-SweepとMark-Compactの組み合わせ

    1. マーク・スウィープ 主に現存するオブジェクトをマーキングするマーカー除去
    2. Mark-Compactは、主にMark-Sweepによって引き起こされるメモリの断片化の問題を解決するためのアルゴリズムで、生存しているオブジェクトをヒープメモリの一方の端にプッシュし、もう一方の端に残っている無駄な参照をすべて一度に削除します。

V8ガベージコレクション機構

メモリリーク

背景:従来の単純なページでは、ページジャンプが完了すると、元のページのメモリ上のデータはすべて破棄されます。現在、SPAページが普及しており、ページジャンプはルーティングによって切り替えられ、その結果、実際にはすべてのサブページが1つのページにとどまり、同じコンテキストメカニズムのセットを共有することになります。 ルーティングされた各ページがメモリリークを気にしない場合、最終的にはページがより多くのメモリを占有することになり、ページのパフォーマンスに影響し、現在のブラウザタブさえクラッシュすることになります。

ゴミ収集メカニズム: ブラウザはオブジェクトをヒープメモリに保持します。屑コレクターは JavaScript エンジンのバックグラウンドプロセスで、アクセスできないオブジェクトを特定し、削除し、基礎となるメモリを取り戻します。

メモリ・リークの原因:メモリ・リークは、ゴミ収集サイクルで一掃されるべきメモリ内のオブジェクトが、意図せず別のオブジェクトへの参照を通じてルートからアクセス可能なままになっている場合に発生します。冗長なオブジェクトをメモリに残しておくと、アプリケーション内でメモリを過剰に使用することになり、パフォーマンスの低下や性能劣化につながります

よくあるメモリリークのケース

  1. 意図せずに作成されたグローバルオブジェクト
  2. 閉鎖:範囲は未公開
  3. タイマー未クリア
  4. イベントリスナーが空にならない
  5. キャッシュ:メモリ上にキャッシュとして存在し、一度も消去されたことのないデータ
  6. 無効な DOM 参照

意図せずに作成されたグローバルオブジェクト

グローバルウィンドウオブジェクトにマウントされた属性値は、ゴミ収集メカニズムによって解放されることのないヒープメモリを占有します。

  1. 非厳密モードでの宣言されていない変数への値の割り当て
  2. グローバルスコープは var 変数
  3. this "をグローバルオブジェクトに向けます。
function createGlobalVariables() {
 leaking1 = 'I leak into the global scope'; // assigning value to the undeclared variable
 this.leaking2 = 'I also leak into the global scope'; // 'this' points to the global object
};
createGlobalVariables();
window.leaking1; // 'I leak into the global scope'
window.leaking2; // 'I also leak into the global scope'

解決策: 厳密モードを使用し、let、const を使用します。

クロージャ

関数スコープの変数は、関数の外部で参照されていなければ、関数がコールスタック を抜けた後にクリアされます。クロージャは、関数の実行が終了して実行コンテキストや変数環境がなくなっても、参照されている変数を保持します。

function outer() {
 const potentiallyHugeArray = [];
 return function inner() {
 potentiallyHugeArray.push('Hello'); // function inner is closed over the potentiallyHugeArray variable
 console.log('Hello');
 };
};
const sayHello = outer(); // contains definition of the function inner
function repeat(fn, num) {
 for (let i = 0; i < num; i++){
 fn();
 }
}
repeat(sayHello, 10); // each sayHello call pushes another 'Hello' to the potentiallyHugeArray 
 
// now imagine repeat(sayHello, )

解決策: a. いつクロージャを作成するか、どのようなオブジェクトを保持するか、クロージャによって参照される不要な参照型を除外する b. クロージャの期待寿命と使用法を理解する。

Timer

タイマーのコールバック関数は、いくつかの参照型を導入するためにクロージャパターンを使用しており、タイマーをクリーンアップするためにclearTimeout/clearIntervalを使用していません。

function setCallback() {
 const data = {
 counter: 0,
 hugeString: new Array().join('x') // ここでのデータは、クロージャとして導入する必要はない。
 };
 return function cb() {
 data.counter++; // data object is now part of the callback's scope
 console.log(data.counter);
 }
}
setInterval(setCallback(), 1000); // how do we stop it?

b. clearIntervalを使ってカウンターを破棄し、不要になったらクリーンアップします。

function setCallback() {
 // 'unpacking' the data object
 let counter = 0;
 const hugeString = new Array().join('x'); // gets removed when the setCallback returns
 return function cb() {
 counter++; // only counter is part of the callback's scope
 console.log(counter);
 }
}
const timerId = setInterval(setCallback(), 1000); // saving the interval ID
// doing something ...
clearInterval(timerId); // stopping the timer i.e. if button pressed

イベントリスニング

アクティブなイベントリスナーは、そのスコープ内に取り込まれたすべての変数がゴミとして収集されるのを防ぎます。a. イベントリスナー関数にバインドされた DOM 要素が removeEventListener() で削除される b. イベントリスナー関数が削除される

const hugeString = new Array().join('x');
document.addEventListener('keyup', function() { // anonymous inline function - can't remove it
 doSomething(hugeString); // hugeString is now forever kept in the callback's scope
});

解決策: a. イベント・リスナーのコールバックとして無名関数を使用する代わりに、名前付き関数を使用します。

function listener() {
 doSomething(hugeString);
}
document.addEventListener('keyup', listener); // named function can be referenced here...
document.removeEventListener('keyup', listener); // 指定されたリスナー関数の呼び出しを削除する

また、addEventListenerの3番目のパラメータを使って、1回だけリスニングを行い、1回トリガーした後は自動的にイベントリスナーから抜けるように指定することもできます。

document.addEventListener('keyup', function listener() {
 doSomething(hugeString);
}, {once: true}); // listener will be removed after running once

キャッシュ

未使用のオブジェクトを削除することなく、またサイズを制限するためのロジックもないまま、キャッシュにメモリが追加され続けると、キャッシュが無制限に増大する可能性があります。参照型に対してキャッシュが行われる場合、元の参照型変数が破棄されたときにキャッシュ内の参照が破棄されるわけではありません。

let user_1 = { name: "Peter", id: 12345 };
let user_2 = { name: "Mark", id: 54321 };
const mapCache = new Map();
function cache(obj){
 if (!mapCache.has(obj)){
 const value = `${obj.name} has an id of ${obj.id}`;
 mapCache.set(obj, value);
 return [value, 'computed'];
 }
 return [mapCache.get(obj), 'cached'];
}
cache(user_1); // ['Peter has an id of 12345', 'computed']
cache(user_1); // ['Peter has an id of 12345', 'cached']
cache(user_2); // ['Mark has an id of 54321', 'computed']
console.log(mapCache); // (( => "Peter has an id of 12345", ( => "Mark has an id of 54321")
user_1 = null; // removing the inactive user
// Garbage Collector
console.log(mapCache); // (( => "Peter has an id of 12345", ( => "Mark has an id of 54321") // first entry is still in cache

解決策:マップ構造の代わりにWeakMap型を使用して、WeakMapに加えて、現在の参照は、任意の外部依存性の参照を持っていないように、正しくゴミが収集されます!

独立した DOM ノード

DOM ノードが JavaScript から直接参照されている場合、たとえそのノードが DOM ツリーから削除されていたとしても、ゴミとして収集されることはありません。

function createElement() {
 const div = document.createElement('div');
 div.id = 'detached';
 return div;
}
// this will keep referencing the DOM element even after deleteElement() is called
const detachedDiv = createElement();
document.body.appendChild(detachedDiv);
function deleteElement() {
 document.body.removeChild(document.getElementById('detached'));
}
deleteElement(); // Heap snapshot will show detached div#detached

document.body.removeChild(document.getElementById('detached'))DOM 要素を削除する呼び出しがあっても、DOM ノードはヒープメモリに残ります。これは、グローバル変数 detachedDiv がそのノードを参照し続けるからです。

解決策: a. 適切な場所で、DOM ノードを参照している変数が null に設定される b. 関数の中で、DOM ノードを参照するためにローカル変数を使用します。

function createElement() {...} // same as above
// DOM references are inside the function scope
function appendElement() {
 const detachedDiv = createElement(); // DOMノードへの参照は関数内で行う。
 document.body.appendChild(detachedDiv);
}
appendElement();
function deleteElement() {
 document.body.removeChild(document.getElementById('detached'));
}
deleteElement(); // no detached div#detached elements in the Heap Snapshot
Read next

Android Studioの設定

kdoc-generatorプラグインを使用すると、通常の複数行コメントを使用するとすぐに関連するパラメータが自動的に生成されます。 SublimeやXcodeと同様に、このプラグインはエディタにコードのサムネイルを埋め込みます。下の画像でわかるように、右側はコードの小さなサムネイルで、スクロールバーは拡大されています。 プレビューコードモードを使って、目的のセクションに素早く移動できます。 使用方法...

Feb 14, 2020 · 3 min read