javascript 內存泄漏

什麼是內存泄漏

簡介

CPU,內存,硬盤的關係

CPU(Central Processing Unit)工做的時候:
  一、須要從存儲器裏取數據出來。
  二、進行運算,要不停地用存儲器讀寫。
  三、計算出結果再返回到存儲器裏。
舉例子形容關係
圖片描述
咱們的PC的APP,手機的APP都是跑在內存上的。
程序的運行須要內存。只要程序提出要求,操做系統就必須供給內存。html

那麼什麼是內存呢?

圖片描述

內存就是處於外存和CPU之間的橋樑,用於存儲CPU的運算數據,這樣內存就能夠保持記憶功能,你寫的全部代碼,都是須要在內存上跑的,虛擬內存是從外存上分配的,很慢
內存的頻率(mhz)越高表明內存運算更快,同一塊內存我跑的更快喲,這就是爲何DDR5比DDR3快的緣由
說這個的緣由,就是若是個人計算機性能足夠好的話,內存泄漏帶來的問題就會愈來愈小。android

那麼什麼是內存溢出呢?

out of memoryios

內存溢出是指程序在申請內存時,沒有足夠的內存空間供其使用,就會出現內存溢出。

在手機上,好比任何一個app,系統初始的時候可能只會給你分配100m的內存,若是有android studio的話,能夠在log上看到,這個時候你點擊了某個圖片列表頁(爲何用圖片舉例,是由於圖片特有的狀況,圖片自己若是是20Kb,長寬爲300的話,渲染到手機上因爲圖片採用的ARGB-888色彩格式,每一個像素點佔用4個字節(雙通道),這樣圖片實際佔用內存就是3003004/1024/1024 = 300+k dpi爲1的狀況),這個時候內存就會暴漲,一旦接近臨界值,程序就會去找操做系統說,我內存不夠了,再給我點,系統就會又給你分配一段,完了你返回首頁了,可是由於你的代碼寫的有問題,暴露各類全局對象啊,各類監聽啊,一進一出屢次,可是系統給每一個app分配的內存是有上限的,直到內存不夠分,泄漏致使的內存溢出。而後crash掉。之前我寫rn的時候,早期的scrollview性能堪憂,出現過內存溢出的現象。程序員

內存泄漏

memory leakweb

內存泄漏指的是你申請了一塊內存,在使用後沒法釋放已申請的內存空間,好比程序會認爲你可能會用到這個變量,就一直給你留着不釋放,一次內存泄漏能夠被忽略,可是內存泄露堆積後果很嚴重,不管多少內存,早晚會被佔光。

既然內存我能夠申請,就能夠被系統回收,在C語言中,須要程序員手動malloc去申請內存,而後free掉它,這寫起來很麻煩,因此其餘大多數語言都提供了自動回收的機制,那麼既然自動回收了,就很容易出現各類問題。算法

內存泄漏的後果

一般來講問題並非特別大,由於正常一個進程的生命週期有限,在當下的大內存快cpu的手機下,影響有限,不過仍是要列舉一些狀況。
1:安卓手機內存管理很差,致使只要不重啓,時間越長,可用內存越少,即便殺程序。具體原因可能還和安卓開放過多權限致使無良app各類保持後臺後門運行也有必定關係。
2:致使內存溢出,若是手機內存被擠佔的有限,那麼手機會變卡,嚴重的本身crash掉,若是是pc端,瀏覽器的內存泄漏致使的溢出會讓瀏覽器出現假死狀態,只能經過強制關閉解決,若是是在webview上,好比我開始的時候寫過一個代碼在ios微信瀏覽器上調用swiper 的3d變換致使微信直接閃退。
3:以上仍是客戶端的,客戶端大多數狀況下不會停留時間過長,因此除非是很是規操做,不多會出大問題,可是,跑在服務端的程序,一般都是一直跑幾天甚至是幾個月的,若是這個裏面有內存泄漏引起的內存溢出的話,那麼就會致使服務器宕機,必須重啓。那帶來的損失就很大了。chrome

引起內存泄漏的方式

1.意外的全局變量

JavaScript 對未聲明變量的處理方式:在全局對象上建立該變量的引用(即全局對象上的屬性,不是變量,由於它能經過delete刪除)。若是在瀏覽器中,全局對象就是window對象。
若是未聲明的變量緩存大量的數據,會致使這些數據只有在窗口關閉或從新刷新頁面時才能被釋放。這樣會形成意外的內存泄漏。瀏覽器

那麼爲何會對未聲明的變量處理方式是掛window下呢?
「當引擎執行LHS查詢時,若是在頂層(全局做用域)中也沒法找到目標變量,全局做用域中就會建立一個具備該名稱的變量,並將其返還給引擎,前提是程序運行在非「嚴格模式」下」緩存

摘錄來自: Kyle Simpson、趙望野、梁傑. 「你不知道的JavaScript(上卷)。」 iBooks.
function foo(arg) {
  bar = 'this is hidden global variable';
}

等同於:安全

function foo(arg) {
  window.bar = 'this is hidden global variable';
}

另外,經過this建立意外的全局變量:

function foo() {
  this.variable = 'this is hidden global variable';
}
// 當在全局做用域中調用foo函數,此時this指向的是全局對象(window),而不是'undefined'
foo();

------------->演示

解決方案

正常的定義全局變量沒有問題,可是這種是屬於意外的泄漏,因此可使用嚴格模式處理,規範本身的代碼。

2.console.log

傳遞給console.log的對象是不能被垃圾回收 ♻️,由於在代碼運行以後須要在開發工具能查看對象信息。因此最好不要在生產環境中console.log任何對象。
追蹤線上問題,console絕非是個好的方式。由於發生問題通常在用戶哪裏,你沒辦法看用戶的日誌。

function aaa() {
    this.name = (Array(100000)).join('*');
    console.log(this);
}
document.getElementsByClassName('console-obj')[0].addEventListener('click', function () {
      var oo = new aaa();
});

------------->演示

解決方案

能夠刪除本身的console.log,可是顯然,在開發環境下,我就是想看個人console.log,這樣註釋來註釋去也挺麻煩的,因此能夠判斷下當前的環境是否是env,若是是product環境下的話,直接

window.console.log = function(){return 'warn:do not use my log'}

這樣的手法不只能夠屏蔽console.log,還能防止別人在咱們的頁面下console.log調試

延伸:如何保護本身的頁面安全

3.閉包(closures)

因爲閉包的特性,經過閉包而能被訪問到的變量,顯然不會被內存回收♻️,由於被回收的話就沒閉包了這個概念了。

function foo() {
      var str = Array(10000).join('#');
      var msg = "test message";
      function unused() {
        var message = 'it is only a test message';
        str = 'unused: ' + str;
      }
      function getData() {
          return msg;
      }
      return getData;
    }
    var bar;
    document.getElementsByClassName('closure-obj')[0].addEventListener('click', function () {
        bar = foo();
    });
    // var list = [];
    // document.getElementsByClassName('closure-obj')[0].addEventListener('click', function () {
    //     list.push(foo());
    // });
  • 演示內存performance狀況
  • 演示memory 狀況
  • 斷點演示閉包scope,call stack

閉包形成的內存泄漏佔用會比其餘的要多。
緣由是在相同做用域內建立的多個內部函數對象是共享同一個變量對象(variable object)。若是建立的內部函數沒有被其餘對象引用,無論內部函數是否引用外部函數的變量和函數,在外部函數執行完,對應變量對象便會被銷燬。反之,若是內部函數中存在有對外部函數變量或函數的訪問(能夠不是被引用的內部函數),而且存在某個或多個內部函數被其餘對象引用,那麼就會造成閉包,外部函數的變量對象就會存在於閉包函數的做用域鏈中。這樣確保了閉包函數有權訪問外部函數的全部變量和函數。

延伸:VO/AO,call stack

解決方案

不暴露到全局變量上,這樣就不會有問題,暴露到全局變量上就手動置爲null,垃圾回收器下次回來會帶走它

4.dom泄漏

在 JavaScript 中,DOM 操做是很是耗時的。由於 JavaScript/ECMAScript 引擎獨立於渲染引擎,而 DOM 是位於渲染引擎,相互訪問須要消耗必定的資源。如 Chrome 瀏覽器中 DOM 位於 WebCore,而 JavaScript/ECMAScript 位於 V8 中。假如將 JavaScript/ECMAScript、DOM 分別想象成兩座孤島,兩島之間經過一座收費橋鏈接,過橋須要交納必定「過橋費」。JavaScript/ECMAScript 每次訪問 DOM 時,都須要交納「過橋費」。所以訪問 DOM 次數越多,費用越高,頁面性能就會受到很大影響。

爲了減小 DOM 訪問次數,通常狀況下,當須要屢次訪問同一個 DOM 方法或屬性時,會將 DOM 引用緩存到一個局部變量中。

但若是在執行某些刪除、更新操做後,可能會忘記釋放掉代碼中對應的 DOM 引用,這樣會形成 DOM 內存泄露。

<input type="button" value="remove" class="remove" style="display:none;">
  <input type="button" value="add" class="add">
  <div class="container">
    <ul class="wrapper"></ul>
  </div>
    // 由於要屢次用到pre.wrapper、div.container、input.remove、input.add節點,將其緩存到本地變量中,
      var wrapper = document.querySelector('.wrapper');
      var container = document.querySelector('.container');
      var removeBtn = document.querySelector('.remove');
      var addBtn = document.querySelector('.add');
      var counter = 0;
      var once = true;
      // 方法
      var hide = function(target){
        target.style.display = 'none';
      }
      var show = function(target){
        target.style.display = 'inline-block';
      }
      // 回調函數
      var removeCallback = function(){
        removeBtn.removeEventListener('click', removeCallback, false);
        addBtn.removeEventListener('click', addCallback, false);
        hide(addBtn);
        hide(removeBtn);
        container.removeChild(wrapper);
        wrapper = null;
      }
      var addCallback = function(){
        let p = document.createElement('li');
        p.appendChild(document.createTextNode("+ ++counter + ':a new line text\n"));
        wrapper.appendChild(p);
        // 顯示刪除操做按鈕
        if(once){
          show(removeBtn);
          once = false;
        }
      }
      // 綁定事件
      removeBtn.addEventListener('click', removeCallback, false);
      addBtn.addEventListener('click', addCallback, false);

--------->演示代碼

var refA = document.getElementById('refA');
    var refB = document.getElementById('refB');
    document.body.removeChild(refA);

    // #refA不能GC回收,由於存在變量refA對它的引用。將其對#refA引用釋放,但仍是沒法回收#refA。
    refA = null;

    // 還存在變量refB對#refA的間接引用(refB引用了#refB,而#refB屬於#refA)。將變量refB對#refB的引用釋放,#refA就能夠被GC回收。
    refB = null;

圖片描述

5.計時器/監聽器

var counter = 0;
    var clock = {
      start: function () {
        // setInterval(this.step, 1000);
        if(!this.timer){
          this.timer = setInterval(this.step, 1000);
        }
      },
      step: function () {
        var date = new Date();
        var h = date.getHours();
        var m = date.getMinutes();
        var s = date.getSeconds();
        console.log('step running');
      }
    }
    // function goo(){
    //     // clock = null;
    //     clearInterval(clock.timer);
    //     console.log('click stop');
    // }
    document.querySelector('.start').addEventListener('click', function () {
      clock.start();
      // document.querySelector('.stop').addEventListener('click',);
    });
    document.querySelector('.stop').addEventListener('click', function () {
      // clock = null;
      clearInterval(clock.timer);
    });

監聽器沒有及時回收或者是匿名回收致使的。
bind,call,apply的區別

如何使用chrome performance

  1. 開啓【Performance】項的記錄
  2. 執行一次 CG,建立基準參考線
  3. 操做頁面
  4. 執行一次 CG
  5. 中止記錄

以上就是咱們使用的時候的步驟
那麼對這個performances裏的各項是如何理解的呢?

前置問題1:什麼是迴流,什麼是重繪,以及爲何迴流必定會致使重繪,可是重繪不會致使迴流?

中置問題2:瀏覽器到了渲染階段的過程是什麼?
圖片描述

一次性能的記錄就完整的展現的瀏覽器的渲染全過程。從圖中也能夠看出,layout後的階段是Painting

跑一個performances

Performances 各項簡介

  • FPS 每秒的幀數,綠色條約稿,表示FPS值越高,一般上面附帶紅色塊的幀表示該幀時間過長,可能須要優化。
  • CPU CPU資源,面積圖表示不一樣事件對CPU資源的消耗。
  • NET 這個項和之前的不同,查詢相關資料也沒有找到到底顯示的是什麼,因此只能經過下面的具體來看,HTML文件是藍色條,腳本文件是黃色條,樣式文件是紫色條,媒體文件是綠色條,其餘的是灰色條,網絡請求部分更詳細的信息建議查看Network。
  • HEAP 內存佔用狀況
  • 三條虛線:藍色指DOMConentLoaded,綠線表示第一次繪製,紅線表示load事件,很明顯看到load是比較慢的。
  • summary loading表明html花的時間,scripting表明腳本的時間,rendering表明計算樣式和迴流花的時間,painting表明繪製的時間
  • Bottom-up 表明花費排序
  • call-tree 表明調用排序
  • event-log 表明各項事務時間線

重點看看這個event-log,以迴流爲例子,再次確認迴流後跟着painting,看看有哪些迴流,而後去看看時間節點,發現對應的頁面出現。
迴流操做仍是挺佔用時間的
以拼團列表圖片高度加載致使的迴流問題,能夠用一個object-fit來搞定常見的狀況

如何規避內存泄漏

注意代碼規範,注意代碼規範,注意代碼規範

垃圾回收

講講垃圾回收,說白了,內存泄漏,溢出,就是由於js有自動垃圾回收的機制,而後自動的垃圾回收器並不能準確的回收你所不想用的東西,就會出一些問題,那麼常見的垃圾回收有兩種

引用計數

當聲明瞭一個變量並將一個引用類型值賦給該變量時,則這個值的引用次數就是 1。 若是同一個值又被賦給另外一個變量,則該值的引用次數加 1。相反,若是包含對這個值引用的變量又取 得了另一個值,則這個值的引用次數減 1。當這個值的引用次數變成 0 時,則說明沒有辦法再訪問這 個值了,於是就能夠將其佔用的內存空間回收回來。這樣,當垃圾收集器下次再運行時,它就會釋放那 些引用次數爲零的值所佔用的內存。

//賦值給o1的對象a{},賦值給o2的對象b{};
var o1 = {
  o2: {
    x: 1
  }
};
//a+1 = 1,b做爲屬性也+1 = 1;
var o3 = o1;
//a+1+1 = 2,b+1+1 = 2                                                 
o1 = 1;     
//a+1+1-1 = 1,b+1+1-1 = 1;
var o4 = o3.o2;
//a+1+1-1 = 1,b+1+1-1+1 = 2;
o3 = '374'; 
//a+1+1-1-1 = 0,b+1+1-1+1-1 = 1;
o4 = null; 
//b-1 = 0;

循環引用致使的問題

//o1:x{},o2:y{};
function f() {
  var o1 = {};
   //x+1 = 1;
  var o2 = {};
    //y+1 = 1;
  o1.p = o2; // o1 references o2
    //y+1+1 = 2;
  o2.p = o1; // o2 references o1. This creates a cycle.
    //x+1+1 = 2;
}
f();

圖片描述

這段代碼o1和o2互相引用致使引用次數回收的時候不爲1,就沒有辦法回收。
假設沒有o2.p= o1這段,那麼o1在出函數的時候要給對應的對象減一,結果發現,o1有一個屬性p還沒解除引用,因此先去解o1.p的,這個時候o2的對象就減一次,完了後o1.p就沒了,那o1就能夠解除o1的對象,o2再-它本身的,都爲0,沒泄漏

反過來,若是上了那段代碼的話,o1要解除,先走p,o1.p想解除,結果發現o2有個p,又去解o2.p,死循環,一個都解不了,仍是2.

假如這個函數被重複屢次調用,就會致使大量內存得 不到回收。爲此,Netscape 在 Navigator 4.0 中放棄了引用計數方式,轉而採用標記清除來實現其垃圾收 集機制。但是,引用計數致使的麻煩並未就此終結。到目前爲止,幾乎全部的瀏覽器都是使用的標記清楚策略,只不過垃圾收集的時間間隔稍微不一樣。

標記清除

當變量進入環境(例如,在函數中聲明一個變量)時,就將這個變量標記爲「進入環境」。從邏輯上講,永遠不能釋放進入環境的變量所佔用的內存,由於只要執行流進入相應的環境,就可能會用到它們。而當變量離開環境時,則將其 標記爲「離開環境」。

Mark and sweep

過去幾年,JavaScript 垃圾回收(代數、增量、並行、並行垃圾收集)領域的全部改進都是對該算法(mark-and-sweep)的實現進行改進,但並無對垃圾回收算法自己進行改進,其目標是肯定一個對象是否可達。
圖片描述
這樣的話,循環引用將再也不是問題
圖片描述
儘管兩個對象仍是存在引用,可是他們從 root 出發已是不可達的了。

總結

在Javascript中,完全避免垃圾回收或者是內存泄漏是很是困難的。因此咱們能作的就是減小泄漏,減小垃圾回收的頻率。對一些高頻使用的函數之類的東西去作一些相似的優化。綜合考慮優化成本