1. 內(nèi)存空間
前言
一直以來(lái)對(duì)JS的理解和認(rèn)識(shí)總是零散雜亂。近期希望整理出一條主線來(lái),把JS的各路好漢串聯(lián)起來(lái)。
我相信很多人和我一樣,對(duì)JS這門動(dòng)態(tài)弱類型語(yǔ)言的學(xué)習(xí)常常是倒過(guò)來(lái)的,就是咱先用著,然后再時(shí)不時(shí)的看些知識(shí)點(diǎn)補(bǔ)充。
為了面試或者裝逼,常常從言語(yǔ)不可描述的角度去看待這門語(yǔ)言,本身無(wú)可厚非。
怎奈我就是一俗人,希望用我粗暴淺顯的理解,去重新認(rèn)識(shí)JavaScript,擁抱JavaScript(此處換成小澤老師、蒼井老師......)。
進(jìn)入正題,可能以前我們并不關(guān)心內(nèi)存空間,從而導(dǎo)致對(duì)內(nèi)存泄露、深淺拷貝等知識(shí)點(diǎn)的理解有點(diǎn)模糊。我的JS主軸線就是從內(nèi)存分配開(kāi)始。
ps: 圖片看不到的,請(qǐng)用chrome、FF、opera。
數(shù)據(jù)結(jié)構(gòu)與算法
原諒我標(biāo)題黨一把,什么數(shù)據(jù)結(jié)構(gòu)與算法都來(lái)了。哈哈哈...
其實(shí)我是想說(shuō)所有的語(yǔ)言都是為了博數(shù)據(jù)一笑而烽火戲程序猿,數(shù)據(jù)的存取當(dāng)然不容忽視,我想從數(shù)據(jù)住的大房子來(lái)開(kāi)始我的重新認(rèn)識(shí)JS之旅。
這樣的大房子(內(nèi)存空間),在所有的編程語(yǔ)言中都擁有相似的生命周期:
- 我愛(ài)你,我給你一棟大房子(內(nèi)存分配)。
- 你懂的...(內(nèi)存使用: 讀、寫)
- 禁不起時(shí)間的考驗(yàn),我要收回大房子,不歡而散。(內(nèi)存釋放--"垃圾回收")
JS作為一門高級(jí)中的VIP的語(yǔ)言。在創(chuàng)建變量的時(shí)候會(huì)為其分配內(nèi)存空間,分配內(nèi)存的舉動(dòng)是在值的初始化、函數(shù)調(diào)用等階段完成。在程序中,使用值的過(guò)程其實(shí)就是對(duì)值的內(nèi)存空間進(jìn)行寫入和讀取。
最后,不再使用的內(nèi)存空間會(huì)被自動(dòng)的進(jìn)行"垃圾回收"。但是確定一個(gè)分配的內(nèi)存空間是不是不再使用確實(shí)讓人頭疼,而且自動(dòng)一詞讓很多人不再關(guān)注于"垃圾回收",這恰恰是一個(gè)美麗的錯(cuò)誤!
我的JS梳理路線第一波:
所以我們需要了解但是不限于以下知識(shí)點(diǎn):
- 內(nèi)存是什么?
- 堆('heap')
- 棧('stack')
- 隊(duì)列('queue')
- 基本類型與引用傳遞
- 深淺拷貝
- 垃圾回收
- 內(nèi)存泄露
- chrome工具進(jìn)行內(nèi)存分析
內(nèi)存是什么?
硬件上計(jì)算機(jī)存儲(chǔ)器由大量的觸發(fā)器組成,觸發(fā)器包含了一些晶體管。每個(gè)觸發(fā)器可以存儲(chǔ)1bit(也叫做"位")。觸發(fā)器有唯一標(biāo)識(shí)用來(lái)尋址,因此我們得以讀取或者覆蓋它們。
觸發(fā)器的組合形成更大的單位,比如8bit為1個(gè)字節(jié)(byte),還有kb...
我們可以抽象理解計(jì)算機(jī)的整個(gè)內(nèi)存是一個(gè)巨大的數(shù)組。
靜態(tài)內(nèi)存分配和動(dòng)態(tài)內(nèi)存分配
對(duì)于原始數(shù)據(jù)類型:
int a; // 4個(gè)字節(jié)
int b[4]; // 4 * 4個(gè)字節(jié)
double c; // 8 個(gè)字節(jié)
編譯器會(huì)檢查數(shù)據(jù)類型并且提前計(jì)算出所需的空間大小(4+4*4+8)。然后為這些原始數(shù)據(jù)變量分配空間,分配的空間我們稱為"棧空間"。假如這些變量定義在一個(gè)函數(shù)中,當(dāng)函數(shù)被調(diào)用的時(shí)候,它們的內(nèi)存就加入到現(xiàn)有的內(nèi)存中,函數(shù)調(diào)用終止,它們就會(huì)被移除。
編譯器能夠準(zhǔn)確知道上面每一個(gè)原始數(shù)據(jù)變量的地址,并且在插入與操作系統(tǒng)交互的代碼的同時(shí)在棧上為其它們申請(qǐng)對(duì)應(yīng)字節(jié)數(shù)的空間。這個(gè)過(guò)程就是靜態(tài)內(nèi)存分配,也有稱之為"自動(dòng)分配"。
如果操作b[4],因?yàn)檫@個(gè)元素并不存在,因?yàn)閿?shù)組長(zhǎng)度為4。所以最終可能讀取(重寫)到c的位。從而導(dǎo)致一些bug。
又如果:
int n = someFuncReturnN(...)
編譯器并不能提前的計(jì)算出變量所需的空間大小,而是在運(yùn)行的時(shí)候才能確定的,這個(gè)時(shí)候不能在棧上為其分配空間了,所以這個(gè)內(nèi)存是分配在堆('heap')空間上的。
堆內(nèi)存涉及指針操作。不再贅述....說(shuō)多了我就懵了。
靜態(tài)內(nèi)存分配和動(dòng)態(tài)內(nèi)存的區(qū)別:
-
靜態(tài)內(nèi)存分配:
- 編譯期知道所需內(nèi)存空間大小。
- 編譯期執(zhí)行
- 申請(qǐng)到棧空間
- FILO(先進(jìn)后出)
-
動(dòng)態(tài)內(nèi)存分配:
- 編譯期不知道所需內(nèi)存空間大小
- 運(yùn)行期執(zhí)行
- 申請(qǐng)到堆空間
- 沒(méi)有特定的順序
總之說(shuō)那么多,還不如一句話:
stack是采用靜態(tài)內(nèi)存分配的內(nèi)存空間,由系統(tǒng)自行釋放。heap是采用動(dòng)態(tài)內(nèi)存分配的內(nèi)存空間,無(wú)序,大小不定,不會(huì)自動(dòng)釋放,哪怕你退出程序,那一塊內(nèi)存還是在那兒。
堆('heap')
臥槽,前邊講多了,這里不知道說(shuō)啥了。反正根據(jù)前邊說(shuō)的動(dòng)態(tài)分配和靜態(tài)分配我們可以知道:
在JavaScript中,引用類型數(shù)據(jù)(對(duì)象、數(shù)組、函數(shù)),這么說(shuō)不太準(zhǔn)確,數(shù)組和函數(shù)也是對(duì)象,就這么地吧。
它們都是申請(qǐng)到堆空間的,然后有一個(gè)引用,可以理解為一個(gè)指針,它保存了這個(gè)對(duì)象在堆中的位置。這個(gè)引用是存到棧中的。
棧('stack')
也叫堆棧。基本數(shù)據(jù)類型String,Boolean之類的變量是申請(qǐng)到棧空間的。
隊(duì)列('queue')
之前看過(guò)一個(gè)段子:
棧和隊(duì)列的區(qū)別? --吃多了拉就是隊(duì)列,吃多了吐就是棧。
這特么也太有才了。不過(guò)說(shuō)明了棧和隊(duì)列的特點(diǎn): 前者先入后出、后者先入先出。
基本類型與引用傳遞
搞清楚內(nèi)存空間,再遇到這種面試題就不會(huì)瑟瑟發(fā)抖了。
var a = 30;
var b = a;
b = 30;
// a是多少?
var obj = {a: 20, b:30}
var newObj = obj;
newObj.a = 25;
// obj.a是多少?
沒(méi)啥說(shuō)的,前者a,b都在棧空間申請(qǐng)了內(nèi)存,var b=a的時(shí)候分配了新的值。兩者互不相干。
后邊的是引用傳遞,兩者指向堆內(nèi)存空間的某個(gè)位置的同一個(gè)對(duì)象。所以對(duì)對(duì)象的操作是互相影響的。
深淺拷貝
淺拷貝:可以理解為只拷貝了1層,如果有數(shù)組之類的對(duì)象的話,實(shí)際是拷貝了其引用。所以操作該對(duì)象是互相影響的。內(nèi)存上是兩個(gè)引用指向了堆空間中的同一對(duì)象
var o = {
name: 'jack ma',
friends: ['李彥宏', '馬化騰']
}
var c = Object.assign({}, o);
c.friends.push('雷軍');
o.friends; // ["李彥宏", "馬化騰", "雷軍"]
深拷貝: 就是遞歸的拷貝,把屬性值也拷貝了。互不影響了。內(nèi)存上是兩個(gè)引用分別指向了堆空間中的不同對(duì)象,但是初始值是一樣的。
var o = {
name: 'jack ma',
friends: ['李彥宏', '馬化騰']
}
var c = JSON.parse(JSON.stringify(o))
c.friends.push('雷軍');
o.friends; // ["李彥宏", "馬化騰"]
垃圾回收
垃圾回收是JS自動(dòng)完成的,但是不代表我們就不去關(guān)注它。實(shí)際上確定一個(gè)內(nèi)存不再被使用,然后將其釋放是很難的。通常有以下幾種算法實(shí)現(xiàn),但是也有很大的局限性。
-
引用計(jì)數(shù)垃圾收集算法
這個(gè)算法是最簡(jiǎn)單的,假如一個(gè)對(duì)象沒(méi)有指針指向它,那它就被認(rèn)為是可回收的。
下面是MDN上面的例子:
var o = { a: { b:2 } }; // 兩個(gè)對(duì)象被創(chuàng)建,一個(gè)作為另一個(gè)的屬性被引用,另一個(gè)被分配給變量o // 很顯然,沒(méi)有一個(gè)可以被垃圾收集 var o2 = o; // o2變量是第二個(gè)對(duì)“這個(gè)對(duì)象”的引用 o = 1; // 現(xiàn)在,“這個(gè)對(duì)象”的原始引用o被o2替換了 var oa = o2.a; // 引用“這個(gè)對(duì)象”的a屬性 // 現(xiàn)在,“這個(gè)對(duì)象”有兩個(gè)引用了,一個(gè)是o2,一個(gè)是oa o2 = "yo"; // 最初的對(duì)象現(xiàn)在已經(jīng)是零引用了 // 他可以被垃圾回收了 // 然而它的屬性a的對(duì)象還在被oa引用,所以還不能回收 oa = null; // a屬性的那個(gè)對(duì)象現(xiàn)在也是零引用了 // 它可以被垃圾回收了
這種算法的局限性體現(xiàn)在循環(huán)引用
function f() { var o1 = {}; var o2 = {}; o1.p = o2; // o1 references o2 o2.p = o1; // o2 references o1. This creates a cycle. } f();
這樣垃圾收集器會(huì)認(rèn)為對(duì)象至少會(huì)被引用一次,而不會(huì)回收這塊內(nèi)存。導(dǎo)致內(nèi)存泄露。
-
標(biāo)記-清除算法
這個(gè)算法是現(xiàn)在瀏覽器基本都有的,其核心思想就是不能被引用的對(duì)象可被回收。
原理大致是:
- 有一個(gè)GC root列表,保存了引用的全局變量,比如 "window".
- root被認(rèn)為是活動(dòng)的,不被回收,然后遞歸檢查其子節(jié)點(diǎn),可以被訪問(wèn)的都標(biāo)記為活動(dòng)的。
- 所有的不被標(biāo)記的,都是可回收的。
[圖片上傳失敗...(image-9cf24e-1511490949760)]
這樣的話,上面的循環(huán)引用,在函數(shù)結(jié)束后,o1,o2不再被全局變量所能訪問(wèn)的對(duì)象引用。就會(huì)被認(rèn)為是垃圾
內(nèi)存泄露
首先GC是無(wú)法預(yù)測(cè)的,其實(shí)回收更多的是取決于我們自己怎么去寫程序。或多或少年少的我們寫的代碼都導(dǎo)致了一些內(nèi)存無(wú)法被釋放,造成了內(nèi)存的泄露。
常見(jiàn)的內(nèi)存泄露
以下都是copy的經(jīng)典例子。
-
全局變量
根據(jù)上邊的標(biāo)記-清除算法,root列表中的全局變量是不會(huì)被釋放的。所以我們的代碼中顯式的全局或者隱式的全局變量是不會(huì)被垃圾收集器回收的。
隱式的情況全局變量有(還有很多):
-
忘記寫聲明了。
function foo(){ boss = 'jack ma' } foo(); window.boss; // "jack ma"
引擎對(duì)boss進(jìn)行LHS查詢,在當(dāng)前作用域沒(méi)有找到聲明,就去外層也就是全局之中找,也特么沒(méi)找到,這個(gè)時(shí)候它就會(huì)發(fā)善心,給你創(chuàng)建一個(gè)聲明。所以輸出window.boss是上面的結(jié)果。
避免這種情況的辦法就是'use strict'。
-
this的默認(rèn)綁定規(guī)則
function foo(){ this.bar = 'jack ma' } foo(); window.boss; // "jack ma"
獨(dú)立的函數(shù)聲明采用的是默認(rèn)綁定規(guī)則,也就說(shuō)this是綁定到全局的。
采用'use strict'可以是默認(rèn)綁定到undefined。
-
-
被遺忘的時(shí)光 | 回憶
定時(shí)器我們常常使用。
var serverData = loadData(); setInterval(function() { var renderer = document.getElementById('renderer'); if(renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000);
IE6時(shí)代,假如serverData有大量的數(shù)據(jù),它是沒(méi)辦法被收集的。但是現(xiàn)代瀏覽器在這個(gè)問(wèn)題已經(jīng)做了優(yōu)化,無(wú)需擔(dān)心。
-
閉包
var theThing = null; var replaceThing = function () { var originalThing = theThing; var unused = function () { if (originalThing) // a reference to 'originalThing' console.log("hi"); }; theThing = { longStr: new Array(1000000).join('*'), someMethod: function () { console.log("message"); } }; }; setInterval(replaceThing, 1000);
一旦具有相同父作用域的多個(gè)閉包的作用域被創(chuàng)建,則這個(gè)作用域就可以被共享。
也就是說(shuō)為someMethod創(chuàng)建的作用域是被unused共享的。theThing作為root持有對(duì)someMethod的引用,unused引用的originalThing,也迫使其不會(huì)被回收。
這個(gè)問(wèn)題是Meteor小組發(fā)現(xiàn)的,有興趣可以百度。
-
脫離DOM的引用
var elements = { button: document.getElementById('button'), image: document.getElementById('image') }; function doStuff() { elements.image.src = 'http://example.com/image_name.png'; } function removeImage() { // 刪除了DOM樹(shù)中對(duì) image 的引用 document.body.removeChild(document.getElementById('image')); // 但是GC并不會(huì)回收。因?yàn)閑lements還引用了呀! }
chrome工具進(jìn)行內(nèi)存分析
利用瀏覽器進(jìn)行內(nèi)存分析具體步驟請(qǐng)執(zhí)行點(diǎn)擊下面的參考最后兩個(gè)。
我們以上邊的閉包為例:
還有各種size之類的我就不說(shuō)了。反正chrome強(qiáng)大的一比!
參考
<a >MDN</a></br>
<a >How JavaScript works: memory management + how to handle 4 common memory leaks</a></br>
<a >Tracing garbage collection</a></br>
<a >ruanyf blog</a></br>
<a >chrome工具進(jìn)行內(nèi)存分析</a>
下一章
<a href='executionContext.md'>執(zhí)行上下文</a>
結(jié)語(yǔ)
擼主實(shí)力有限,高手歷來(lái)在民間,希望廣提意見(jiàn),補(bǔ)腎感激。歡迎star,對(duì)我也是一種鼓勵(lì)。