ホームページ  >  記事  >  ウェブフロントエンド  >  一般的な JavaScript のメモリ リーク

一般的な JavaScript のメモリ リーク

小云云
小云云オリジナル
2017-12-05 16:31:451686ブラウズ

メモリ リークとは

メモリ リークとは、プログラムが過失やエラーにより使用されなくなったメモリを解放できないことを指します。メモリ リークとは、メモリの物理的な消失を指すのではなく、アプリケーションがメモリの特定のセグメントを割り当てた後、設計上のエラーにより、解放される前にメモリのセグメントの制御を失い、メモリの無駄遣い。
メモリリークは通常、プログラムのソースコードにアクセスできるプログラマのみが分析できます。ただし、厳密には正確ではありませんが、メモリ使用量の望ましくない増加をメモリ リークと表現することに慣れている人がかなりいます。
————wikipedia

予期しないグローバル変数

JavaScript が宣言されていない変数を処理する方法: グローバル オブジェクトの変数への参照を作成します (つまり、変数ではなく、グローバル オブジェクトのプロパティです。削除で削除できます)。ブラウザーを使用している場合、グローバル オブジェクトはウィンドウ オブジェクトです。

宣言されていない変数が大量のデータをキャッシュしている場合、データはウィンドウが閉じられるか、ページが更新されるときにのみ解放されます。これにより、予期しないメモリ リークが発生する可能性があります。

<span style="font-size: 14px;">function foo(arg) {<br>    bar = "this is a hidden global variable with a large of data";<br>}<br></span>

は次と同等です:

<span style="font-size: 14px;">function foo(arg) {<br>    window.bar = "this is an explicit global variable with a large of data";<br>}<br></span>

さらに、これを通じて予期しないグローバル変数を作成します:

<span style="font-size: 14px;">function foo() {<br>    this.variable = "potential accidental global";<br>}<br><br>// 当在全局作用域中调用foo函数,此时this指向的是全局对象(window),而不是'undefined'<br>foo();<br></span>

解決策:

JavaScript ファイルに「use strict」を追加して、strict モードをオンにします、上記の問題を効果的に回避できます。

<span style="font-size: 14px;">function foo(arg) {<br>    "use strict" // 在foo函数作用域内开启严格模式<br>    bar = "this is an explicit global variable with a large of data";// 报错:因为bar还没有被声明<br>}<br></span>

関数でグローバル変数を使用する必要がある場合は、次のコードに示すようにウィンドウ上で明示的に宣言できます:

<span style="font-size: 14px;">function foo(arg) {<br>    window.bar = "this is a explicit global variable with a large of data";<br>}<br></span>

これは可読性が高いだけでなく、後のメンテナンスにも便利です

グローバル変数について言えば、大量のデータを一時的に保存するために使用されるグローバル変数に注意する必要があります。データを処理した後は、必ず null に設定するか、値を再割り当てしてください。グローバル変数はキャッシュとしてもよく使用されます。パフォーマンスを最適化するには、キャッシュのサイズに上限を設定するのが最善です。キャッシュは再利用できないため、キャッシュが多いほどメモリ消費量も多くなります。

console.log

console.log: Web 開発コンソールにメッセージを出力します。開発中のデバッグや分析によく使用されます。開発中にオブジェクト情報を出力する必要がある場合がありますが、公開時に console.log ステートメントを削除するのを忘れると、メモリ リークが発生する可能性があります。

console.log に渡されたオブジェクトはガベージ コレクションできません ♻️ これは、コードの実行後にオブジェクト情報を開発ツールで表示する必要があるためです。したがって、運用環境ではオブジェクトを console.log に記録しないことが最善です。

例----->demos/log.html

<span style="font-size: 14px;"><!DOCTYPE html><br><html lang="en"><br><br><head><br>  <meta charset="UTF-8"><br>  <meta name="viewport" content="width=device-width, initial-scale=1.0"><br>  <meta http-equiv="X-UA-Compatible" content="ie=edge"><br>  <title>Leaker</title><br></head><br><br><body><br>  <input type="button" value="click"><br>  <script><br>    !function () {<br>      function Leaker() {<br>        this.init();<br>      };<br>      Leaker.prototype = {<br>        init: function () {<br>          this.name = (Array(100000)).join('*');<br>          console.log("Leaking an object %o: %o", (new Date()), this);// this对象不能被回收<br>        },<br><br>        destroy: function () {<br>          // do something....<br>        }<br>      };<br>      document.querySelector('input').addEventListener('click', function () {<br>        new Leaker();<br>      }, false);<br>    }()<br>  </script><br></body><br><br></html><br></span>

ここではChromeのDevtools–>パフォーマンスと組み合わせて、分析を行います。手順は次のとおりです:

⚠️注) : ブラウザのプラグインが解析結果に影響を与えないように、非表示のウィンドウで解析作業を実行するのが最善です

  1. [パフォーマンス]項目の記録を有効にします

  2. CGを一度実行して、ベースライン基準線

  3. ボタンを3回連続クリックして3つのLeakerオブジェクトを作成

  4. CGを1回実行

  5. 記録を停止

一般的な JavaScript のメモリ リーク

それ[JS Heap] 行が最終レベルに戻っていないことがわかります。ベースライン参照行の位置には、明らかに回収されていないメモリがあります。コードを次のように変更すると、console.log("Leaking an object %o: %o", (new Date()), this); ステートメントを削除します。上記の手順を繰り返すと、分析結果は次のようになります:

比較分析結果から、console.log によって出力されたオブジェクトがガベージ コレクターによってリサイクルされないことがわかります。したがって、特に運用環境では、ページの全体的なパフォーマンスに影響を与える可能性があるため、ページ内の大きなオブジェクトを console.log に記録しないことをお勧めします。 console.log に加えて、console.dir、console.error、console.warn などにも同様の問題があります。これらの詳細には特別な注意が必要です。 一般的な JavaScript のメモリ リーク

closures(闭包)

当一个函数A返回一个内联函数B,即使函数A执行完,函数B也能访问函数A作用域内的变量,这就是一个闭包——————本质上闭包是将函数内部和外部连接起来的一座桥梁。

<span style="font-size: 14px;">function foo(message) {<br>    function closure() {<br>        console.log(message)<br>    };<br>    return closure;<br>}<br><br>// 使用<br>var bar = foo("hello closure!");<br>bar()// 返回 'hello closure!'<br></span>

在函数foo内创建的函数closure对象是不能被回收掉的,因为它被全局变量bar引用,处于一直可访问状态。通过执行bar()可以打印出hello closure!。如果想释放掉可以将bar = null即可。

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。

实例------>demos/closures.html

<span style="font-size: 14px;"><!DOCTYPE html><br><html lang="en"><br><br><head><br>  <meta charset="UTF-8"><br>  <meta name="viewport" content="width=device-width, initial-scale=1.0"><br>  <meta http-equiv="X-UA-Compatible" content="ie=edge"><br>  <title>Closure</title><br></head><br><br><body><br>  <p>不断单击【click】按钮</p><br>  <button id="click_button">Click</button><br>  <script><br>    function f() {<br>      var str = Array(10000).join('#');<br>      var foo = {<br>        name: 'foo'<br>      }<br>      function unused() {<br>        var message = 'it is only a test message';<br>        str = 'unused: ' + str;<br>      }<br>      function getData() {<br>        return 'data';<br>      }<br>      return getData;<br>    }<br><br>    var list = [];<br>    <br>    document.querySelector('#click_button').addEventListener('click', function () {<br>      list.push(f());<br>    }, false);<br>  </script><br></body><br><br></html><br></span>

这里结合Chrome的Devtools->Memory工具进行分析,操作步骤如下:

⚠️注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果

  1. 选中【Record allocation timeline】选项

  2. 执行一次CG

  3. 单击【start】按钮开始记录堆分析

  4. 连续单击【click】按钮十多次

  5. 停止记录堆分析

一般的な JavaScript のメモリ リーク

上图中蓝色柱形条表示随着时间新分配的内存。选中其中某条蓝色柱形条,过滤出对应新分配的对象:

一般的な JavaScript のメモリ リーク

查看对象的详细信息:

一般的な JavaScript のメモリ リーク

从图可知,在返回的闭包作用链(Scopes)中携带有它所在函数的作用域,作用域中还包含一个str字段。而str字段并没有在返回getData()中使用过。为什么会存在在作用域中,按理应该被GC回收掉, why

原因是在相同作用域内创建的多个内部函数对象是共享同一个变量对象(variable object)。如果创建的内部函数没有被其他对象引用,不管内部函数是否引用外部函数的变量和函数,在外部函数执行完,对应变量对象便会被销毁。反之,如果内部函数中存在有对外部函数变量或函数的访问(可以不是被引用的内部函数),并且存在某个或多个内部函数被其他对象引用,那么就会形成闭包,外部函数的变量对象就会存在于闭包函数的作用域链中。这样确保了闭包函数有权访问外部函数的所有变量和函数。了解了问题产生的原因,便可以对症下药了。对代码做如下修改:

<span style="font-size: 14px;">    function f() {<br>      var str = Array(10000).join('#');<br>      var foo = {<br>        name: 'foo'<br>      }<br>      function unused() {<br>        var message = 'it is only a test message';<br>        // str = 'unused: ' + str; //删除该条语句<br>      }<br>      function getData() {<br>        return 'data';<br>      }<br>      return getData;<br>    }<br><br>    var list = [];<br>    <br>    document.querySelector('#click_button').addEventListener('click', function () {<br>      list.push(f());<br>    }, false);<br></span>

getData()和unused()内部函数共享f函数对应的变量对象,因为unused()内部函数访问了f作用域内str变量,所以str字段存在于f变量对象中。加上getData()内部函数被返回,被其他对象引用,形成了闭包,因此对应的f变量对象存在于闭包函数的作用域链中。这里只要将函数unused中str = 'unused: ' + str;语句删除便可解决问题。

一般的な JavaScript のメモリ リーク

查看一下闭包信息:

一般的な JavaScript のメモリ リーク

DOM泄露

在JavaScript中,DOM操作是非常耗时的。因为JavaScript/ECMAScript引擎独立于渲染引擎,而DOM是位于渲染引擎,相互访问需要消耗一定的资源。如Chrome浏览器中DOM位于WebCore,而JavaScript/ECMAScript位于V8中。假如将JavaScript/ECMAScript、DOM分别想象成两座孤岛,两岛之间通过一座收费桥连接,过桥需要交纳一定“过桥费”。JavaScript/ECMAScript每次访问DOM时,都需要交纳“过桥费”。因此访问DOM次数越多,费用越高,页面性能就会受到很大影响。了解更多ℹ️

一般的な JavaScript のメモリ リーク

为了减少DOM访问次数,一般情况下,当需要多次访问同一个DOM方法或属性时,会将DOM引用缓存到一个局部变量中。但如果在执行某些删除、更新操作后,可能会忘记释放掉代码中对应的DOM引用,这样会造成DOM内存泄露。

实例------>demos/dom.html

<span style="font-size: 14px;"><!DOCTYPE html><br><html lang="en"><br><head><br>  <meta charset="UTF-8"><br>  <meta name="viewport" content="width=device-width, initial-scale=1.0"><br>  <meta http-equiv="X-UA-Compatible" content="ie=edge"><br>  <title>Dom-Leakage</title><br></head><br><body><br>  <input type="button" value="remove" class="remove"><br>  <input type="button" value="add" class="add"><br><br>  <p class="container"><br>    <pre class="wrapper">