JavaScript中的垃圾回收和內存泄漏

摘要: JS內存管理。javascript

Fundebug經受權轉載,版權歸原做者全部。html

前言

程序的運行須要內存。只要程序提出要求,操做系統或者運行時就必須供給內存。所謂的內存泄漏簡單來講是再也不用到的內存,沒有及時釋放。爲了更好避免內存泄漏,咱們先介紹Javascript垃圾回收機制。前端

在C與C++等語言中,開發人員能夠直接控制內存的申請和回收。可是在Java、C#、JavaScript語言中,變量的內存空間的申請和釋放都由程序本身處理,開發人員不須要關心。也就是說Javascript具備自動垃圾回收機制(Garbage Collecation)。java

1、垃圾回收的必要性

下面這段話引自《JavaScript權威指南(第四版)》node

因爲字符串、對象和數組沒有固定大小,全部當他們的大小已知時,才能對他們進行動態的存儲分配。JavaScript程序每次建立字符串、數組或對象時,解釋器都必須分配內存來存儲那個實體。只要像這樣動態地分配了內存,最終都要釋放這些內存以便他們可以被再用,不然,JavaScript的解釋器將會消耗完系統中全部可用的內存,形成系統崩潰。chrome

這段話解釋了爲何須要系統須要垃圾回收,JavaScript不像C/C++,它有本身的一套垃圾回收機制。小程序

JavaScript垃圾回收的機制很簡單:找出再也不使用的變量,而後釋放掉其佔用的內存,可是這個過程不是時時的,由於其開銷比較大,因此垃圾回收器會按照固定的時間間隔週期性的執行。segmentfault

var a = "浪裏行舟";
var b = "前端工匠";
var a = b; //重寫a

這段代碼運行以後,「浪裏行舟」這個字符串失去了引用(以前是被a引用),系統檢測到這個事實以後,就會釋放該字符串的存儲空間以便這些空間能夠被再利用。微信小程序

2、垃圾回收機制

垃圾回收機制怎麼知道,哪些內存再也不須要呢?數組

垃圾回收有兩種方法:標記清除、引用計數。引用計數不太經常使用,標記清除較爲經常使用。

1.標記清除

這是javascript中最經常使用的垃圾回收方式。當變量進入執行環境是,就標記這個變量爲「進入環境」。從邏輯上講,永遠不能釋放進入環境的變量所佔用的內存,由於只要執行流進入相應的環境,就可能會用到他們。當變量離開環境時,則將其標記爲「離開環境」。

垃圾收集器在運行的時候會給存儲在內存中的全部變量都加上標記。而後,它會去掉環境中的變量以及被環境中的變量引用的標記。而在此以後再被加上標記的變量將被視爲準備刪除的變量,緣由是環境中的變量已經沒法訪問到這些變量了。最後。垃圾收集器完成內存清除工做,銷燬那些帶標記的值,並回收他們所佔用的內存空間。

咱們用個例子,解釋下這個方法:

var m = 0,n = 19 // 把 m,n,add() 標記爲進入環境。
add(m, n) // 把 a, b, c標記爲進入環境。
console.log(n) // a,b,c標記爲離開環境,等待垃圾回收。
function add(a, b) {
  a++
  var c = a + b
  return c
}

2. 引用計數

所謂"引用計數"是指語言引擎有一張"引用表",保存了內存裏面全部的資源(一般是各類值)的引用次數。若是一個值的引用次數是0,就表示這個值再也不用到了,所以能夠將這塊內存釋放。

上圖中,左下角的兩個值,沒有任何引用,因此能夠釋放。

若是一個值再也不須要了,引用數卻不爲0,垃圾回收機制沒法釋放這塊內存,從而致使內存泄漏。

var arr = [1, 2, 3, 4];
arr = [2, 4, 5]
console.log('浪裏行舟');

上面代碼中,數組[1, 2, 3, 4]是一個值,會佔用內存。變量arr是僅有的對這個值的引用,所以引用次數爲1。儘管後面的代碼沒有用到arr,它仍是會持續佔用內存。至於如何釋放內存,咱們下文介紹。

第三行代碼中,數組[1, 2, 3, 4]引用的變量arr又取得了另一個值,則數組[1, 2, 3, 4]的引用次數就減1,此時它引用次數變成0,則說明沒有辦法再訪問這個值了,於是就能夠將其所佔的內存空間給收回來。

可是引用計數有個最大的問題: 循環引用

function func() {
    let obj1 = {};
    let obj2 = {};

    obj1.a = obj2; // obj1 引用 obj2
    obj2.a = obj1; // obj2 引用 obj1
}

當函數 func 執行結束後,返回值爲 undefined,因此整個函數以及內部的變量都應該被回收,但根據引用計數方法,obj1 和 obj2 的引用次數都不爲 0,因此他們不會被回收。

要解決循環引用的問題,最好是在不使用它們的時候手工將它們設爲空。上面的例子能夠這麼作:

obj1 = null;
obj2 = null;

3、哪些狀況會引發內存泄漏?

雖然JavaScript會自動垃圾收集,可是若是咱們的代碼寫法不當,會讓變量一直處於「進入環境」的狀態,沒法被回收。下面列一下內存泄漏常見的幾種狀況:

1. 意外的全局變量

function foo(arg) {
    bar = "this is a hidden global variable";
}

bar沒被聲明,會變成一個全局變量,在頁面關閉以前不會被釋放。

另外一種意外的全局變量可能由 this 建立:

function foo() {
    this.variable = "potential accidental global";
}
// foo 調用本身,this 指向了全局對象(window)
foo();

在 JavaScript 文件頭部加上 'use strict',能夠避免此類錯誤發生。啓用嚴格模式解析 JavaScript ,避免意外的全局變量。

2. 被遺忘的計時器或回調函數

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 處理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

這樣的代碼很常見,若是id爲Node的元素從DOM中移除,該定時器仍會存在,同時,由於回調函數中包含對someResource的引用,定時器外面的someResource也不會被釋放。

3. 閉包

function bindEvent(){
  var obj=document.createElement('xxx')
  obj.onclick=function(){
    // Even if it is a empty function
  }
}

閉包能夠維持函數內局部變量,使其得不到釋放。上例定義事件回調時,因爲是函數內定義函數,而且內部函數--事件回調引用外部函數,造成了閉包。

// 將事件處理函數定義在外面
function bindEvent() {
  var obj = document.createElement('xxx')
  obj.onclick = onclickHandler
}
// 或者在定義事件處理函數的外部函數中,刪除對dom的引用
function bindEvent() {
  var obj = document.createElement('xxx')
  obj.onclick = function() {
    // Even if it is a empty function
  }
  obj = null
}

解決之道,將事件處理函數定義在外部,解除閉包,或者在定義事件處理函數的外部函數中,刪除對dom的引用。

4. 沒有清理的DOM元素引用

有時,保存 DOM 節點內部數據結構頗有用。假如你想快速更新表格的幾行內容,把每一行 DOM 存成字典(JSON 鍵值對)或者數組頗有意義。此時,一樣的 DOM 元素存在兩個引用:一個在 DOM 樹中,另外一個在字典中。未來你決定刪除這些行時,須要把兩個引用都清除。

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};
function doStuff() {
    image.src = 'http://some.url/image';
    button.click();
    console.log(text.innerHTML);
}
function removeButton() {
    document.body.removeChild(document.getElementById('button'));
    // 此時,仍舊存在一個全局的 #button 的引用
    // elements 字典。button 元素仍舊在內存中,不能被 GC 回收。
}

雖然咱們用removeChild移除了button,可是還在elements對象裏保存着#button的引用,換言之,DOM元素還在內存裏面。

4、內存泄漏的識別方法

新版本的chrome在 performance 中查看:

步驟:

  • 打開開發者工具 Performance
  • 勾選 Screenshots 和 memory
  • 左上角小圓點開始錄製(record)
  • 中止錄製

圖中 Heap 對應的部分就能夠看到內存在週期性的回落也能夠看到垃圾回收的週期,若是垃圾回收以後的最低值(咱們稱爲min),min在不斷上漲,那麼確定是有較爲嚴重的內存泄漏問題。

避免內存泄漏的一些方式:

  • 減小沒必要要的全局變量,或者生命週期較長的對象,及時對無用的數據進行垃圾回收
  • 注意程序邏輯,避免「死循環」之類的
  • 避免建立過多的對象

總而言之須要遵循一條原則:不用了的東西要及時歸還

5、垃圾回收的使用場景優化

1. 數組array優化

將[]賦值給一個數組對象,是清空數組的捷徑(例如: arr = [];),可是須要注意的是,這種方式又建立了一個新的空對象,而且將原來的數組對象變成了一小片內存垃圾!實際上,將數組長度賦值爲0(arr.length = 0)也能達到清空數組的目的,而且同時能實現數組重用,減小內存垃圾的產生。

const arr = [1, 2, 3, 4];
console.log('浪裏行舟');
arr.length = 0  // 能夠直接讓數字清空,並且數組類型不變。
// arr = []; 雖然讓a變量成一個空數組,可是在堆上從新申請了一個空數組對象。

2. 對象儘可能複用

對象儘可能複用,尤爲是在循環等地方出現建立新對象,能複用就複用。不用的對象,儘量設置爲null,儘快被垃圾回收掉。

var t = {} // 每次循環都會建立一個新對象。
for (var i = 0; i < 10; i++) {
  // var t = {};// 每次循環都會建立一個新對象。
  t.age = 19
  t.name = '123'
  t.index = i
  console.log(t)
}
t = null //對象若是已經不用了,那就當即設置爲null;等待垃圾回收。

3. 在循環中的函數表達式,能複用最好放到循環外面。

// 在循環中最好也別使用函數表達式。
for (var k = 0; k < 10; k++) {
  var t = function(a) {
    // 建立了10次  函數對象。
    console.log(a)
  }
  t(k)
}
// 推薦用法
function t(a) {
  console.log(a)
}
for (var k = 0; k < 10; k++) {
  t(k)
}
t = null

參考資料

關於Fundebug

Fundebug專一於JavaScript、微信小程序、微信小遊戲、支付寶小程序、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有Google、360、金山軟件、百姓網等衆多品牌企業。歡迎你們免費試用

相關文章
相關標籤/搜索