js內(nèi)存深入學習(一)

一. 內(nèi)存空間儲存

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

1. 數(shù)據(jù)結(jié)構(gòu)類型

  • 棧: 后進先出(LIFO)的數(shù)據(jù)結(jié)構(gòu)
  • 堆: 一種樹狀結(jié)構(gòu)
  • 隊列: 先進先出(FIFO)的數(shù)據(jù)結(jié)構(gòu)

2. 變量的存放

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

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

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

變量的存放

在計算機的數(shù)據(jù)結(jié)構(gòu)中,棧比堆的運算速度快,Object是一個復雜的結(jié)構(gòu)且可以擴展:數(shù)組可擴充,對象可添加屬性,都可以增刪改查。將他們放在堆中是為了不影響棧的效率。而是通過引用的方式查找到堆中的實際對象再進行操作。所以查找引用類型值的時候先去棧查找再去堆查找。

例子:

<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都是指向?qū)ο驛的。

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

    • 我們知道js的賦值運算順序永遠都是從右往左的,不過由于“.”是優(yōu)先級最高的運算符,所以這行代碼先“計算”了a.x。a指向的對象{n:1}新增了屬性x(雖然這個x是undefined的)
    • 依循“從右往左”的賦值運算順序先執(zhí)行 a={n:2} ,這時候,a指向的對象發(fā)生了改變,變成了新對象{n:2}(我們稱為對象B)
    • 接著繼續(xù)執(zhí)行 a.x=a, 由于一開始js已經(jīng)先計算了a.x,便已經(jīng)解析了這個a.x是對象A的x,所以在同一條公式的情況下再回來給a.x賦值,所以應理解為對象A的屬性x指向了對象B。

另外, 閉包中的變量并不保存中棧內(nèi)存中,而是保存在堆內(nèi)存中,這也就解釋了函數(shù)之后之后為什么閉包還能引用到函數(shù)內(nèi)的變量。

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

函數(shù) A 彈出調(diào)用棧后,函數(shù) A 中的變量這時候是存儲在堆上的,所以函數(shù)B依舊能引用到函數(shù)A中的變量?,F(xiàn)在的 JS 引擎可以通過逃逸分析辨別出哪些變量需要存儲在堆上,哪些需要存儲在棧上。

二. 內(nèi)存空間管理

1. 內(nèi)存生命周期

JavaScript的內(nèi)存生命周期是

1、分配你所需要的內(nèi)存

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

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

JavaScript有自動垃圾收集機制,垃圾收集器會每隔一段時間就執(zhí)行一次釋放操作,找出那些不再繼續(xù)使用的值,然后釋放其占用的內(nèi)存。

  • 局部變量和全局變量的銷毀
    • 局部變量:局部作用域中,當函數(shù)執(zhí)行完畢,局部變量也就沒有存在的必要了,因此垃圾收集器很容易做出判斷并回收。
    • 全局變量:全局變量什么時候需要自動釋放內(nèi)存空間則很難判斷,所以在開發(fā)中盡量避免使用全局變量。
  • 以Google的V8引擎為例,V8引擎中所有的JS對象都是通過堆來進行內(nèi)存分配的
    • 初始分配:當聲明變量并賦值時,V8引擎就會在堆內(nèi)存中分配給這個變量。
    • 繼續(xù)申請:當已申請的內(nèi)存不足以存儲這個變量時,V8引擎就會繼續(xù)申請內(nèi)存,直到堆的大小達到了V8引擎的內(nèi)存上限為止。
  • V8引擎對堆內(nèi)存中的JS對象進行分代管理
    • 新生代:存活周期較短的JS對象,如臨時變量、字符串等。
    • 老生代:經(jīng)過多次垃圾回收仍然存活,存活周期較長的對象,如主控制器、服務器對象等。

2. 垃圾回收算法

  • 2.1 引用計數(shù)(現(xiàn)代瀏覽器不再使用)

引用計數(shù)算法簡單理解,就是看一個對象是否有指向它的引用。如果沒有其他對象指向它了,說明該對象已經(jīng)不再需要了。

// 創(chuàng)建一個對象person,他有兩個指向?qū)傩詀ge和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對象已經(jīng)沒有引用,很快會被回收

引用計數(shù)有一個致命的問題,那就是循環(huán)引用

如果兩個對象相互引用,盡管他們已不再使用,但是垃圾回收器不會進行回收,最終可能會導致內(nèi)存泄露。

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

    return "cycle reference!"
}

cycle();

cycle函數(shù)執(zhí)行完成之后,對象o1和o2實際上已經(jīng)不再需要了,但根據(jù)引用計數(shù)的原則,他們之間的相互引用依然存在,因此這部分內(nèi)存不會被回收。所以現(xiàn)代瀏覽器不再使用這個算法。

但是IE依舊使用,如下,變量div有事件處理函數(shù)的引用,同時事件處理函數(shù)也有div的引用,因為div變量可在函數(shù)內(nèi)被訪問,所以循環(huán)引用就出現(xiàn)了。

var div = document.createElement("div");
div.onclick = function() {
    console.log("click");
};
  • 2.2 標記清除(常用)

標記清除算法將“不再使用的對象”定義為“無法到達的對象”。即從根部(在JS中就是全局對象)出發(fā)定時掃描內(nèi)存中的對象,凡是能從根部到達的對象,保留。那些從根部出發(fā)無法觸及到的對象被標記為不再使用,稍后進行回收。所以像上面的例子,雖然是循環(huán)引用,但從全局來說并沒有被使用到,所以就可以正確被垃圾回收處理了。

算法由以下幾步組成:

  • 垃圾回收器創(chuàng)建了一個“roots”列表。roots通常是代碼中全局變量的引用。JavaScript 中,“window”對象是一個全局變量,被當作 root 。window對象總是存在,因此垃圾回收器可以檢查它和它的所有子對象是否存在(即不是圾);
  • 所有的 roots 被檢查和標記為激活(即不是垃圾)。所有的子對象也被遞歸地查。從 root 開始的所有對象如果是可達的,它就不被當作垃圾。
  • 所有未被標記的內(nèi)存會被當做垃圾,收集器現(xiàn)在可以釋放內(nèi)存,歸還給操作系了。

對于主流瀏覽器來說,只需要切斷需要回收的對象與根部的聯(lián)系。但可能還存在著與DOM元素綁定有關的內(nèi)存問題:

email.message = document.createElement(“div”);
displayList.appendChild(email.message);

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

上面代碼中,div元素已經(jīng)從DOM樹中清除,但是該div元素還綁定在email對象中,所以如果email對象存在,那么該div元素就會一直保存在內(nèi)存中。如果不再需要使用的話,需要手動設置email.message = null。

另外ES6 新出的兩種數(shù)據(jù)結(jié)構(gòu):WeakSet 和 WeakMap,表示這是弱引用,它們對于值的引用都是不計入垃圾回收機制的。

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

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

先新建一個 Weakmap 實例,然后將一個 DOM 節(jié)點作為鍵名存入該實例,并將一些附加信息作為鍵值,一起存放在 WeakMap 里面。這時,WeakMap 里面對element的引用就是弱引用,不會被計入垃圾回收機制。

繼下一篇文章: js內(nèi)存深入學習(二)

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容