js內存深刻學習(一)

一. 內存空間儲存

某些狀況下,調用堆棧中函數調用的數量超出了調用堆棧的實際大小,瀏覽器會拋出一個錯誤終止運行。這個就涉及到內存問題了。html

1. 數據結構類型

  • 棧: 後進先出(LIFO)的數據結構 棧
  • 堆: 一種樹狀結構
  • 隊列: 先進先出(FIFO)的數據結構 隊列

2. 變量的存放

JS內存空間分爲棧(stack)、堆(heap)、池(通常也會歸類爲棧中)。 其中棧存放變量,堆存放複雜對象,池存放常量,因此也叫常量池。算法

一、基本類型 --> 保存在棧內存中,由於這些類型在內存中分別佔有固定大小的空間,經過按值來訪問。基本類型一共有6種:Undefined、Null、Boolean、Number 、String和Symbol數組

二、引用類型 --> 保存在堆內存中,由於這種值的大小不固定,所以不能把它們保存到棧內存中,但內存地址大小的固定的,所以保存在堆內存中,在棧內存中存放的只是該對象的訪問地址。當查詢引用類型的變量時, 先從棧中讀取內存地址, 而後再經過地址找到堆中的值。對於這種,咱們把它叫作按引用訪問。瀏覽器

變量的存放

在計算機的數據結構中,棧比堆的運算速度快,Object是一個複雜的結構且能夠擴展:數組可擴充,對象可添加屬性,均可以增刪改查。將他們放在堆中是爲了避免影響棧的效率。而是經過引用的方式查找到堆中的實際對象再進行操做。因此查找引用類型值的時候先去棧查找再去堆查找。服務器

例子:數據結構

<script>
var a = {n:1}; 
var b = a;  
a.x = a = {n:2}; 
console.log(a.x);// --> undefined 
console.log(b.x);// --> {n:2}
</script>

  

解析:閉包

  1. var a = {n:1}; var b = a; 在這裏a指向了一個對象{n:1}(咱們姑且稱它爲對象A),b指向了a所指向的對象,也就是說,在這時候a和b都是指向對象A的。app

  2. a.x = a = {n:2};函數

    • 咱們知道js的賦值運算順序永遠都是從右往左的,不過因爲「.」是優先級最高的運算符,因此這行代碼先「計算」了a.x。a指向的對象{n:1}新增了屬性x(雖然這個x是undefined的)
    • 依循「從右往左」的賦值運算順序先執行 a={n:2} ,這時候,a指向的對象發生了改變,變成了新對象{n:2}(咱們稱爲對象B)
    • 接着繼續執行 a.x=a, 因爲一開始js已經先計算了a.x,便已經解析了這個a.x是對象A的x,因此在同一條公式的狀況下再回來給a.x賦值,因此應理解爲對象A的屬性x指向了對象B。

另外, 閉包中的變量並不保存中棧內存中,而是保存在堆內存中,這也就解釋了函數以後以後爲何閉包還能引用到函數內的變量。學習

function A() {
  let a = 1
  function B() {
      console.log(a)
  }
  return B
}

  

函數 A 彈出調用棧後,函數 A 中的變量這時候是存儲在堆上的,因此函數B依舊能引用到函數A中的變量。如今的 JS 引擎能夠經過逃逸分析辨別出哪些變量須要存儲在堆上,哪些須要存儲在棧上。

二. 內存空間管理

1. 內存生命週期

JavaScript的內存生命週期是

一、分配你所須要的內存

二、使用分配到的內存(讀、寫)

三、不須要時將其釋放、歸還

JavaScript有自動垃圾收集機制,垃圾收集器會每隔一段時間就執行一次釋放操做,找出那些再也不繼續使用的值,而後釋放其佔用的內存。

  • 局部變量和全局變量的銷燬
    • 局部變量:局部做用域中,當函數執行完畢,局部變量也就沒有存在的必要了,所以垃圾收集器很容易作出判斷並回收。
    • 全局變量:全局變量何時須要自動釋放內存空間則很難判斷,因此在開發中儘可能避免使用全局變量。
  • 以Google的V8引擎爲例,V8引擎中全部的JS對象都是經過堆來進行內存分配的
    • 初始分配:當聲明變量並賦值時,V8引擎就會在堆內存中分配給這個變量。
    • 繼續申請:當已申請的內存不足以存儲這個變量時,V8引擎就會繼續申請內存,直到堆的大小達到了V8引擎的內存上限爲止。
  • V8引擎對堆內存中的JS對象進行分代管理
    • 新生代:存活週期較短的JS對象,如臨時變量、字符串等。
    • 老生代:通過屢次垃圾回收仍然存活,存活週期較長的對象,如主控制器、服務器對象等。

2. 垃圾回收算法

  • 2.1 引用計數(現代瀏覽器再也不使用)

引用計數算法簡單理解,就是看一個對象是否有指向它的引用。若是沒有其餘對象指向它了,說明該對象已經再也不須要了。

// 建立一個對象person,他有兩個指向屬性age和name的引用
var person = {
    age: 12,
    name: 'aaaa'
};

person.name = null; // 雖然name設置爲null,但由於person對象還有指向name的引用,所以name不會回收

var p = person; 
person = 1;         //原來的person對象被賦值爲1,但由於有新引用p指向原person對象,所以它不會被回收

p = null;           //原person對象已經沒有引用,很快會被回收

  

引用計數有一個致命的問題,那就是循環引用

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

function cycle() {
    var o1 = {};
    var o2 = {};
    o1.a = o2;
    o2.a = o1; 

    return "cycle reference!"
}

cycle();

  

cycle函數執行完成以後,對象o1和o2實際上已經再也不須要了,但根據引用計數的原則,他們之間的相互引用依然存在,所以這部份內存不會被回收。因此現代瀏覽器再也不使用這個算法。

可是IE依舊使用,以下,變量div有事件處理函數的引用,同時事件處理函數也有div的引用,由於div變量可在函數內被訪問,因此循環引用就出現了。

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

  

  • 2.2 標記清除(經常使用)

標記清除算法將「再也不使用的對象」定義爲「沒法到達的對象」。即從根部(在JS中就是全局對象)出發定時掃描內存中的對象,凡是能從根部到達的對象,保留。那些從根部出發沒法觸及到的對象被標記爲再也不使用,稍後進行回收。因此像上面的例子,雖然是循環引用,但從全局來講並無被使用到,因此就能夠正確被垃圾回收處理了。

算法由如下幾步組成:

  • 垃圾回收器建立了一個「roots」列表。roots一般是代碼中全局變量的引用。JavaScript 中,「window」對象是一個全局變量,被看成 root 。window對象老是存在,所以垃圾回收器能夠檢查它和它的全部子對象是否存在(即不是圾);
  • 全部的 roots 被檢查和標記爲激活(即不是垃圾)。全部的子對象也被遞歸地查。從 root 開始的全部對象若是是可達的,它就不被看成垃圾。
  • 全部未被標記的內存會被當作垃圾,收集器如今能夠釋放內存,歸還給操做繫了。

對於主流瀏覽器來講,只須要切斷須要回收的對象與根部的聯繫。但可能還存在着與DOM元素綁定有關的內存問題:

email.message = document.createElement(「div」);
displayList.appendChild(email.message);

// 稍後從displayList中清除DOM元素
displayList.removeAllChildren();

 

上面代碼中,div元素已經從DOM樹中清除,可是該div元素還綁定在email對象中,因此若是email對象存在,那麼該div元素就會一直保存在內存中。若是再也不須要使用的話,須要手動設置email.message = null。

另外ES6 新出的兩種數據結構:WeakSet 和 WeakMap,表示這是弱引用,它們對於值的引用都是不計入垃圾回收機制的。

const wm = new WeakMap();
const element = document.getElementById('example');

wm.set(element, 'some information');
wm.get(element) // "some information"

  

先新建一個 Weakmap 實例,而後將一個 DOM 節點做爲鍵名存入該實例,並將一些附加信息做爲鍵值,一塊兒存放在 WeakMap 裏面。這時,WeakMap 裏面對element的引用就是弱引用,不會被計入垃圾回收機制。

 

續篇 js內存深刻學習(二)

相關文章
相關標籤/搜索