js內存管理

前言

像C語言這樣的底層語言通常都有底層的內存管理接口,好比 malloc()free()。相反,JavaScript是在建立變量(對象,字符串等)時自動進行了分配內存,而且在不使用它們時「自動」釋放。 釋放的過程稱爲垃圾回收。這個「自動」是混亂的根源,並讓JavaScript(和其餘高級語言)開發者錯誤的感受他們能夠不關心內存管理。node

內存的生命週期

生命週期概念

不管是是使用什麼編程語言,內存生命週期幾乎都是同樣的:程序員

生命週期的概述:算法

  • 內存分配(Allocate memory ):當咱們申明變量、函數、對象的時候,系統會自動爲他們分配內存
  • 內存使用(Use memory ):即讀寫內存,也就是使用變量、函數等
  • 內存釋放(Release memory ):使用完畢,由垃圾回收機制自動回收再也不使用的內存

內存的概念

在硬件層面,計算機內存是由大量的觸發器組成的。每個觸發器都包含有一些晶體管,可以存儲1比特。單個觸發器可經過一個惟一標識符來尋址,這樣咱們就能夠讀和寫了。所以從概念上講,咱們能夠把計算機內存看做是一個巨大的比特數組,咱們能夠對它進行讀和寫。編程

可是做爲人類,咱們並不善於用比特來思考和運算,所以咱們將其組成更大些的分組,這樣咱們就能夠用來表示數字。8個比特就是一個字節。比字節大的有字(16比特或32比特)。數組

有不少東西都存儲在內存中:瀏覽器

  1. 全部被程序使用的變量和其餘數據
  2. 程序的代碼,包括操做系統自身的代碼

當你編譯你的代碼時,編譯器能夠檢查原始的數據類型而且提早計算出將會須要多少內存。而後把所需的(內存)容量分配給調用棧空間中的程序。這些變量由於函數被調用而分配到的空間被稱爲堆棧空間,它們的內存增長在現存的內存上面(累加)。如它們再也不被須要就會按照 LIFO(後進,先出)的順序被移除。例如,參見以下聲明:緩存

int n; // 4 bytes
int x[4]; // array of 4 elements, each 4 bytes
double m; // 8 bytes
複製代碼

編譯器能夠當即清楚這段代碼須要4 + 4 × 4 + 8 = 28字節。bash

這就是它怎樣工做於當前的 integers 和 doubles 型的大小。約20年前,integers一般(佔用)2字節,double佔4字節。你的代碼不該該依賴於此時刻的基本數據類型的大小。數據結構

編譯器將插入些會互相做用於操做系統在堆棧上去請求必要的字節數來存儲變量代碼。閉包

在以上例子中,編譯器知道每一個變量精確的內存地址。事實上,不管咱們什麼時候寫入變量n,而本質上這會被翻譯爲如「內存地址 4127963 」。

JS內存分配

爲了避免讓程序員費心分配內存,JavaScript 在定義變量時就完成了內存分配。

//給數值分配內存空間
var num = 1; 

//給字符串分配內存
var str = "hehe";

//給對象及其包含的值分配內存
var obj = {
  a: 1,
  b: "str",
  c: null
}

// 給數組及其包含的值分配內存(就像對象同樣)
var a = [1, null, "abra"]; 

// 給函數(可調用的對象)分配內存
function f(a){
  return a + 2;
} 

// 函數表達式也能分配一個對象
someElement.addEventListener('click', function(){
  someElement.style.backgroundColor = 'blue';
}, false);
複製代碼

有些函數調用結果是分配對象內存 以下:

var d = new Date(); // 分配一個 Date 對象

var e = document.createElement('div'); // 分配一個 DOM 元素
複製代碼

有些方法是分配新變量或者新對象 以下:

var s = "azerty";
var s2 = s.substr(0, 3); // s2 是一個新的字符串
// 由於字符串是不變量,
// JavaScript 可能決定不分配內存,
// 只是存儲了 [0-3] 的範圍。

var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2); 
// 新數組有四個元素,是 a 鏈接 a2 的結果
複製代碼

JS使用內存

基本上在 JavaScript 中使用分配的內存,就是對它進行讀和寫操做。

能夠讀寫變量的值或某個對象的屬性,甚至是給某個函數傳遞一個參數。

var a = 10; // 分配內存
console.log(a); // 對內存的使用
複製代碼

JS內存回收

當內存再也不須要的時候要釋放掉

大部分的內存管理問題出如今這個階段。

這裏面最難的任務是指出,在何時分配的內存再也不被須要。這一般須要開發者來決定程序中的那一塊內存再也不須要了,並釋放。

高級語言嵌入了一個叫垃圾收集器的程序,它能夠跟蹤內存分配和使用狀況,以找出在哪一種狀況下某一塊已分配的內存再也不被須要,並自動的釋放它。

不幸的是,這種程序只是一種近似的操做,由於知道某塊內存是否被須要是不可斷定的(並不能經過算法來解決)。

大部分的垃圾收集器的工做方式是收集那些不可以被再次訪問的內存,好比超出做用域的變量。可是,可以被收集的內存空間是低於近似值的,由於在任什麼時候候均可能存在一個在做用域內的變量指向一塊內存區域,可是它永遠不可以被再次訪問。

再也不須要使用的變量也就是生命週期結束的變量,是局部變量,局部變量只在函數的執行過程當中存在, 當函數運行結束,沒有其餘引用(閉包),那麼該變量會被標記回收。

全局變量的生命週期直至瀏覽器卸載頁面纔會結束,也就是說全局變量不會被當成垃圾回收。

由於自動垃圾回收機制的存在,開發人員能夠不關心也不注意內存釋放的有關問題,但對無用內存的釋放這件事是客觀存在的。 不幸的是,即便不考慮垃圾回收對性能的影響,目前最新的垃圾回收算法,也沒法智能回收全部的極端狀況。

垃圾回收

引用

垃圾回收算法主要依賴於引用的概念。

在內存管理的環境中,一個對象若是有訪問另外一個對象的權限(隱式或者顯式),叫作一個對象引用另外一個對象。

例如,一個Javascript對象具備對它原型的引用(隱式引用)和對它屬性的引用(顯式引用)。

在這裏,「對象」的概念不只特指 JavaScript 對象,還包括函數做用域(或者全局詞法做用域)。

引用計數垃圾收集

這是最初級的垃圾回收算法。

引用計數算法定義「內存再也不使用」的標準很簡單,就是看一個對象是否有指向它的引用。 若是沒有其餘對象指向它了,說明該對象已經再也不需了。

var o = { 
  a: {
    b:2
  }
}; 
// 兩個對象被建立,一個做爲另外一個的屬性被引用,另外一個被分配給變量o
// 很顯然,沒有一個能夠被垃圾收集


var o2 = o; // o2變量是第二個對「這個對象」的引用

o = 1;      // 如今,「這個對象」的原始引用o被o2替換了

var oa = o2.a; // 引用「這個對象」的a屬性
// 如今,「這個對象」有兩個引用了,一個是o2,一個是oa

o2 = "yo"; // 最初的對象如今已是零引用了
           // 他能夠被垃圾回收了
           // 然而它的屬性a的對象還在被oa引用,因此還不能回收

oa = null; // a屬性的那個對象如今也是零引用了
           // 它能夠被垃圾回收了
複製代碼
複製代碼

由上面能夠看出,引用計數算法是個簡單有效的算法。但它卻存在一個致命的問題:循環引用。

若是兩個對象相互引用,儘管他們已再也不使用,垃圾回收不會進行回收,致使內存泄露。

來看一個循環引用的例子:

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o  這裏

  return "azerty";
}

f();
複製代碼
複製代碼

上面咱們申明瞭一個函數 f ,其中包含兩個相互引用的對象。 在調用函數結束後,對象 o1 和 o2 實際上已離開函數範圍,所以再也不須要了。 但根據引用計數的原則,他們之間的相互引用依然存在,所以這部份內存不會被回收,內存泄露不可避免了。

再來看一個實際的例子:

var div = document.createElement("div");
div.onclick = function() {
    console.log("click");
};
複製代碼
複製代碼

上面這種JS寫法再普通不過了,建立一個DOM元素並綁定一個點擊事件。 此時變量 div 有事件處理函數的引用,同時事件處理函數也有div的引用!(div變量可在函數內被訪問)。 一個循序引用出現了,按上面所講的算法,該部份內存無可避免的泄露了。

爲了解決循環引用形成的問題,現代瀏覽器經過使用標記清除算法來實現垃圾回收。

標記清除算法

標記清除算法將「再也不使用的對象」定義爲「沒法達到的對象」。 簡單來講,就是從根部(在JS中就是全局對象)出發定時掃描內存中的對象。 凡是能從根部到達的對象,都是還須要使用的。 那些沒法由根部出發觸及到的對象被標記爲再也不使用,稍後進行回收。

從這個概念能夠看出,沒法觸及的對象包含了沒有引用的對象這個概念(沒有任何引用的對象也是沒法觸及的對象)。 但反之未必成立。

工做流程:

  1. 垃圾收集器會在運行的時候會給存儲在內存中的全部變量都加上標記。
  2. 從根部出發將能觸及到的對象的標記清除。
  3. 那些還存在標記的變量被視爲準備刪除的變量。
  4. 最後垃圾收集器會執行最後一步內存清除的工做,銷燬那些帶標記的值並回收它們所佔用的內存空間。

img

循環引用再也不是問題了

再看以前循環引用的例子:

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();
複製代碼
複製代碼

函數調用返回以後,兩個循環引用的對象在垃圾收集時從全局對象出發沒法再獲取他們的引用。 所以,他們將會被垃圾回收器回收。

內存泄露

概念

程序的運行須要內存。只要程序提出要求,操做系統或者運行時(runtime)就必須供給內存。

對於持續運行的服務進程(daemon),必須及時釋放再也不用到的內存。 不然,內存佔用愈來愈高,輕則影響系統性能,重則致使進程崩潰。

本質上講,內存泄漏就是因爲疏忽或錯誤形成程序未能釋放那些已經再也不使用的內存,形成內存的浪費。

常見的JS內存泄露

1.意外的全局變量

JavaScript 處理未定義變量的方式比較寬鬆:未定義的變量會在全局對象建立一個新變量。在瀏覽器中,全局對象是 window 。

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

事實上變量bar被解釋成下面的狀況:

function foo(arg) { 
    window.bar = "this is a hidden global variable"; 
}
複製代碼

函數 foo 內部忘記使用 var ,意外建立了一個全局變量。此例泄露了一個簡單的字符串,無傷大雅,可是有更糟的狀況。

由this建立的意外的全局變量:

function foo() { 
    this.variable = "potential accidental global"; 
} 
 
// Foo 調用本身,this 指向了全局對象(window) 
// 而不是 undefined 
foo()
複製代碼

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

全局變量使用注意事項

  1. 儘管咱們討論了一些意外的全局變量,可是仍有一些明確的全局變量產生的垃圾。它們被定義爲不可回收(除非定義爲空或從新分配)。
  2. 全局變量用於 臨時存儲和處理大量信息時,須要多加當心。若是必須使用全局變量存儲大量數據時,確保用完之後把它設置爲 null 或者從新定義
  3. 與全局變量相關的增長內存消耗的一個主因是緩存。緩存數據是爲了重用,緩存必須有一個大小上限纔有用。
  4. 高內存消耗致使緩存突破上限,由於緩存內容沒法被回收。

2.沒有釋放的計時器或者回調函數

在 JavaScript 中使用 setInterval 很是日常。一段常見的代碼:

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

與節點或數據關聯的計時器再也不須要,node 對象能夠刪除,整個回調函數也不須要了。但是,計時器回調函數仍然沒被回收(計時器中止纔會被回收)。同時,someResource 若是存儲了大量的數據,也是沒法被回收的。

對於觀察者的例子,一旦它們再也不須要(或者關聯的對象變成不可達),明確地移除它們很是重要。老的 IE 6 是沒法處理循環引用的。現在,即便沒有明確移除它們,一旦觀察者對象變成不可達,大部分瀏覽器是能夠回收觀察者處理函數的。

var element = document.getElementById('button'); 
function onClick(event) { 
    element.innerHTML = 'text'; 
} 
 
element.addEventListener('click', onClick); 

複製代碼

對象觀察者和循環引用注意事項

老版本的 IE 是沒法檢測 DOM 節點與 JavaScript 代碼之間的循環引用,會致使內存泄露。現在,現代的瀏覽器(包括 IE 和 Microsoft Edge)使用了更先進的垃圾回收算法,已經能夠正確檢測和處理循環引用了。換言之,回收節點內存時,沒必要非要調用 removeEventListener 了。

3.脫離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() { 
    // 按鈕是 body 的後代元素 
    document.body.removeChild(document.getElementById('button')); 
 
    // 此時,仍舊存在一個全局的 #button 的引用 
    // elements 字典。button 元素仍舊在內存中,不能被 GC 回收。 
複製代碼

此外還要考慮 DOM 樹內部或子節點的引用問題。假如你的 JavaScript 代碼中保存了表格某一個 的引用。未來決定刪除整個表格的時候,直覺認爲 GC 會回收除了已保存的 之外的其它節點。實際狀況並不是如此:此 是表格的子節點,子元素與父元素是引用關係。因爲代碼保留了 的引用,致使整個表格仍待在內存中。保存 DOM 元素引用的時候,要當心謹慎。

4.閉包

閉包是 JavaScript 開發的一個關鍵方面:匿名函數能夠訪問父級做用域的變量。

var theThing = null; 
var replaceThing = function () { 
  var originalThing = theThing; 
  var unused = function () { 
    if (originalThing) 
      console.log("hi"); 
  }; 
 
  theThing = { 
    longStr: new Array(1000000).join('*'), 
    someMethod: function () { 
      console.log(someMessage); 
    } 
  }; 
}; 
 
setInterval(replaceThing, 1000); 
複製代碼

每次調用 replaceThing ,theThing 獲得一個包含一個大數組和一個新閉包(someMethod)的新對象。同時,變量 unused 是一個引用 originalThing 的閉包(先前的 replaceThing 又調用了 theThing )。思緒混亂了嗎?最重要的事情是,閉包的做用域一旦建立,它們有一樣的父級做用域,做用域是共享的。someMethod 能夠經過 theThing 使用,someMethod 與 unused 分享閉包做用域,儘管 unused從未使用,它引用的 originalThing 迫使它保留在內存中(防止被回收)。當這段代碼反覆運行,就會看到內存佔用不斷上升,垃圾回收器(GC)並沒有法下降內存佔用。本質上,閉包的鏈表已經建立,每個閉包做用域攜帶一個指向大數組的間接的引用,形成嚴重的內存泄露。

避免內存泄露

記住一個原則:不用的東西,及時歸還。

  1. 減小沒必要要的全局變量,使用嚴格模式避免意外建立全局變量。
  2. 在你使用完數據後,及時解除引用(閉包中的變量,dom引用,定時器清除)。
  3. 組織好你的邏輯,避免死循環等形成瀏覽器卡頓,崩潰的問題。
相關文章
相關標籤/搜索