容易被遺忘的前端基礎:JavaScript 內存詳解

目錄

JS內存目錄

簡介

某些語言,好比C有低級的原生內存管理原語,像malloc()和free()。開發人員使用這些原語能夠顯式分配和釋放操做系統的內存。javascript

相對地,JavaScript會在建立變量(對象、字符串)時自動分配內存,並在這些變量不被使用時自動釋放內存,這個過程被稱爲垃圾回收。這個「自動」釋放資源的特性帶來了不少困惑,讓JavaScript(和其餘高級級語言)開發者誤覺得能夠不關心內存管理。這是一個很大的錯誤html

內存生命週期

不管使用什麼編程語言,內存生命週期基本是一致的: 前端

內存生命週期圖

  • 分配內存: 內存被操做系統分配,容許程序使用它 (當申明變量、函數、對象的時候,系統會自動爲他們分配內存)
  • 使用內存:經過在代碼操做變量對內在進行讀和寫 (也就是使用變量、函數等)
  • 釋放內存:不用的時候,就能夠釋放內存,以便從新分配 (由垃圾回收機制自動回收再也不使用的內存)

JS 內存模型

在JavaScript中的內存空間分爲兩種:棧內存(stack)堆內存(heap), 而JavaScript的數據類型也分爲兩大類, 分別是基本數據類型引用數據類型。 這些數據類型在內存中是怎樣存儲的?java

說是JS內存模型其實不太準確,只是便於理解。因爲JavaScript中的內存分配是由js引擎完成的,因此更準確的描述是js引擎的內存模型。
複製代碼

基礎數據類型與棧內存

JS中的基礎數據類型,這些值都有固定的大小,每每都保存在棧內存中(閉包除外),由系統自動分配存儲空間。咱們能夠直接操做保存在棧內存空間的值,所以基礎數據類型都是按值訪問數據在棧內存中的存儲與使用方式相似於數據結構中的堆棧數據結構,遵循後進先出的原則。git

所熟知的基礎數據類型:github

NumberString、Null、Boolean、Undefiend、Symbol(ES6新增)
複製代碼

簡單理解棧的存取方式,咱們能夠經過類比乒乓球盒子來分析。以下圖左側。 算法

棧存取方式

這種乒乓球的存放方式與棧中存取數據的方式一模一樣。處於盒子中最頂層的乒乓球5,它必定是最後被放進去,但能夠最早被使用。而咱們想要使用底層的乒乓球1,就必須將上面的4個乒乓球取出來,讓乒乓球1處於盒子頂層。這就是棧空間先進後出,後進先出的特色。編程

引用數據類型與堆內存

與其餘語言不一樣,JS的引用數據類型,好比數組Array,它們值的大小是不固定的。引用數據類型的值是保存在堆內存中的對象。JavaScript不容許直接訪問堆內存中的位置,所以咱們不能直接操做對象的堆內存空間。在操做對象時,其實是在操做對象的引用而不是實際的對象。所以,引用類型的值都是按引用訪問的。這裏的引用,咱們能夠粗淺地理解爲保存在變量對象中的一個地址,該地址與堆內存的實際值相關聯。segmentfault

堆數據結構是一種樹狀結構。它的存取數據的方式,則與書架與書很是類似。數組

所熟知的引用數據類型:

ObjectArrayDateRegExpFunction 等。
複製代碼

爲了更好的搞懂變量對象與堆內存,咱們能夠結合如下例子與圖解進行理解。

var a1 = 0;   // 變量對象
var a2 = 'this is string'; // 變量對象
var a3 = null; // 變量對象

var b = { m: 20 }; // 變量b存在於變量對象中,{m: 20} 做爲對象存在於堆內存中
var c = [1, 2, 3]; // 變量c存在於變量對象中,[1, 2, 3] 做爲對象存在於堆內存中
複製代碼

Javascript內存詳解-2

所以當咱們要訪問堆內存中的引用數據類型時,實際上咱們首先是從變量對象中獲取了該對象的地址引用(或者地址指針),而後再從堆內存中取得咱們須要的數據。

理解了JS的內存空間,咱們就能夠藉助內存空間的特性來驗證一下引用類型的一些特色了。

接下來,咱們經過下面的例子來加深對JS內存的理解

var a = 20;
var b = a;
b = 30;

var m = { a: 10, b: 20 };
var n = m;
n.a = 15; 
複製代碼

此時a的值是什麼? 而m.a的值又是什麼?

Javascript內存詳解-3

在變量對象中的數據發生複製行爲時,系統會自動爲新的變量分配一個新值。var b = a執行以後,a與b雖然值都等於20,可是他們其實已是相互獨立互不影響的值了。具體如圖。因此咱們修改了b的值之後,a的值並不會發生變化。

Javascript內存詳解-4

經過var n = m執行一次複製引用類型的操做。引用類型的複製一樣也會爲新的變量自動分配一個新的值保存在變量對象中,但不一樣的是,這個新的值,僅僅只是引用類型的一個地址指針。當地址指針相同時,儘管他們相互獨立,可是在變量對象中訪問到的具體對象其實是同一個。

垃圾回收

垃圾回收是一種內存管理機制,就是將再也不用到的內存及時釋放,以防內存佔用愈來愈高,致使卡頓甚至進程崩潰。在JavaScript中有垃圾回收機制,其做用就是自動回收過時無效的變量。

在JavaScript中內存垃圾回收是由js引擎自動完成的。實現垃圾回收的關鍵在於如何肯定內存再也不使用,也就是肯定對象是否無用。主要有兩種方式:引用計數 和 標記清除。

垃圾回收算法

引用計數(reference counting)

這是IE六、7採用的一種比較老的垃圾回收機制。這是最初級的垃圾收集算法。此算法把「對象是否再也不須要」簡化定義爲「對象有沒有其餘對象引用到它」。若是沒有引用指向該對象(零引用),對象將被垃圾回收機制回收。

var o1 = {
  o2: {
    x: 1
  }
};

//2個對象被建立
/'o2''o1'做爲屬性引用
//誰也不能被回收

var o3 = o1; //'o3'是第二個引用'o1'指向對象的變量

o1 = 1;      //如今,'o1'只有一個引用了,就是'o3'
var o4 = o3.o2; // 引用'o3'對象的'o2'屬性
                //'o2'對象這時有2個引用: 一個是做爲對象的屬性
                //另外一個是'o4'

o3 = '374'; //'o1'原來的對象如今有0個對它的引用
             //'o1'能夠被垃圾回收了。
            //然而它的'o2'屬性依然被'o4'變量引用,因此'o2'不能被釋放。

o4 = null;  //最初'o1'中的'o2'屬性沒有被其餘的引用了
           //'o2'能夠被垃圾回收了
複製代碼

循環引用創造麻煩 在涉及循環引用的時候有一個限制。在下面的例子中,兩個對象被建立了,並且相互引用,這樣建立了一個循環引用。它們會在函數調用後超出做用域,應該能夠釋放。然而引用計數算法考慮到2個對象中的每個至少被引用了一次,所以都不能夠被回收。

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; // o1 引用 o2
  o2.p = o1; // o2 引用 o1\. 造成循環引用
}

f();
複製代碼

Javascript內存詳解-6

標記清除(mark and sweep)

工做原理簡化後就是:從垃圾收集根(root)對象(在JavaScript中爲全局環境記錄)開始,標記出全部能夠得到的對象,而後清除掉全部未標記的不可得到的對象。

這個算法把「對象是否再也不須要」簡化定義爲「對象是否能夠得到」。 算法包含如下步驟。

  • 垃圾回收器生成一個根列表。根一般是將引用保存在代碼中的全局變量。在JavaScript中,window對象是一個能夠做爲根的全局變量。
  • 全部的根都被檢查和標記成活躍的(不是垃圾),全部的子變量也被遞歸檢查。全部可能從根元素到達的都不被認爲是垃圾。
  • 全部沒有被標記成活躍的內存都被認爲是垃圾。垃圾回收器就能夠釋放內存而且把內存還給操做系統。

Javascript內存詳解-7

2012年起,全部瀏覽器都內置了標記清除垃圾回收器。

內存泄漏

內存泄漏基本上就是再也不被應用須要的內存,因爲某種緣由,沒有被歸還給操做系統或者進入可用內存池。 簡單來講: 就是再也不用到的內存,沒有及時釋放,就叫作內存泄漏(memory leak)。

Chrome 瀏覽器查看內存佔用

按照如下步驟操做

  • 打開Chrome瀏覽器開發者工具的Performance面板
  • 選項欄中勾選Memory選項
  • 點擊左上角錄製按鈕(實心圓狀按鈕)
  • 在頁面上進行正常操做
  • 一段時間後,點擊Stop,觀察面板上的數據

Javascript內存詳解-8

更多方式查看內存佔用,點擊這裏

4種常見的JavaScript內存泄漏

    1. 意外的全局變量
    1. 被遺忘的定時器或者回調
    1. 閉包
    1. DOM外引用

閉包自己不會形成內存泄露,程序寫錯了纔會形成內存泄漏或者閉包過多很容易致使內存泄漏。

具體詳情點擊【譯】JavaScript是如何工做的:內存管理 + 如何處理4個常見的內存泄露

總結

JS內存詳解

JS內存詳解

原文地址

參考資料

相關文章
相關標籤/搜索