1. 程式人生 > >js內存深入學習(一)

js內存深入學習(一)

棧內存 fun 解釋 content fifo ring asc 成了 undefine

一. 內存空間儲存

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

1. 數據結構類型

  • 棧: 後進先出(LIFO)的數據結構 技術分享圖片
  • 堆: 一種樹狀結構
  • 隊列: 先進先出(FIFO)的數據結構 技術分享圖片

2. 變量的存放

JS內存空間分為棧(stack)、堆(heap)、池(一般也會歸類為棧中)。 其中棧存放變量,堆存放復雜對象,池存放常量,所以也叫常量池。

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

2、引用類型 --> 保存在堆內存中,因為這種值的大小不固定,因此不能把它們保存到棧內存中,但內存地址大小的固定的,因此保存在堆內存中,在棧內存中存放的只是該對象的訪問地址。當查詢引用類型的變量時, 先從棧中讀取內存地址, 然後再通過地址找到堆中的值。對於這種,我們把它叫做按引用訪問。

技術分享圖片

在計算機的數據結構中,棧比堆的運算速度快,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的。

  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的內存生命周期是

1、分配你所需要的內存

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

3、不需要時將其釋放、歸還

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內存深入學習(一)