一. 內(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>
解析:
var a = {n:1}; var b = a;在這里a指向了一個對象{n:1}(我們姑且稱它為對象A),b指向了a所指向的對象,也就是說,在這時候a和b都是指向?qū)ο驛的。-
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)存深入學習(二)