1. 內(nèi)存空間
前言
一直以來對JS的理解和認(rèn)識(shí)總是零散雜亂房交。近期希望整理出一條主線來挪蹭,把JS的各路好漢串聯(lián)起來诵棵。
我相信很多人和我一樣舶胀,對JS這門動(dòng)態(tài)弱類型語言的學(xué)習(xí)常常是倒過來的,就是咱先用著网梢,然后再時(shí)不時(shí)的看些知識(shí)點(diǎn)補(bǔ)充震缭。
為了面試或者裝逼,常常從言語不可描述的角度去看待這門語言战虏,本身無可厚非拣宰。
怎奈我就是一俗人,希望用我粗暴淺顯的理解烦感,去重新認(rèn)識(shí)JavaScript巡社,擁抱JavaScript(此處換成小澤老師、蒼井老師......)手趣。
進(jìn)入正題晌该,可能以前我們并不關(guān)心內(nèi)存空間肥荔,從而導(dǎo)致對內(nèi)存泄露、深淺拷貝等知識(shí)點(diǎn)的理解有點(diǎn)模糊朝群。我的JS主軸線就是從內(nèi)存分配開始燕耿。
ps: 圖片看不到的,請用chrome姜胖、FF誉帅、opera。
數(shù)據(jù)結(jié)構(gòu)與算法
原諒我標(biāo)題黨一把右莱,什么數(shù)據(jù)結(jié)構(gòu)與算法都來了蚜锨。哈哈哈...
其實(shí)我是想說所有的語言都是為了博數(shù)據(jù)一笑而烽火戲程序猿,數(shù)據(jù)的存取當(dāng)然不容忽視慢蜓,我想從數(shù)據(jù)住的大房子來開始我的重新認(rèn)識(shí)JS之旅亚再。
這樣的大房子(內(nèi)存空間),在所有的編程語言中都擁有相似的生命周期:
- 我愛你晨抡,我給你一棟大房子(內(nèi)存分配)氛悬。
- 你懂的...(內(nèi)存使用: 讀、寫)
- 禁不起時(shí)間的考驗(yàn)凄诞,我要收回大房子,不歡而散忍级。(內(nèi)存釋放--"垃圾回收")
JS作為一門高級(jí)中的VIP的語言帆谍。在創(chuàng)建變量的時(shí)候會(huì)為其分配內(nèi)存空間,分配內(nèi)存的舉動(dòng)是在值的初始化轴咱、函數(shù)調(diào)用等階段完成汛蝙。在程序中,使用值的過程其實(shí)就是對值的內(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í)用來尋址般甲,因此我們得以讀取或者覆蓋它們肋乍。
觸發(fā)器的組合形成更大的單位,比如8bit為1個(gè)字節(jié)(byte)敷存,還有kb...
我們可以抽象理解計(jì)算機(jī)的整個(gè)內(nèi)存是一個(gè)巨大的數(shù)組墓造。
靜態(tài)內(nèi)存分配和動(dòng)態(tài)內(nèi)存分配
對于原始數(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í)在棧上為其它們申請對應(yīng)字節(jié)數(shù)的空間。這個(gè)過程就是靜態(tài)內(nèi)存分配硫惕,也有稱之為"自動(dòng)分配"茧痕。
如果操作b[4],因?yàn)檫@個(gè)元素并不存在恼除,因?yàn)閿?shù)組長度為4踪旷。所以最終可能讀取(重寫)到c的位。從而導(dǎo)致一些bug豁辉。
又如果:
int n = someFuncReturnN(...)
編譯器并不能提前的計(jì)算出變量所需的空間大小,而是在運(yùn)行的時(shí)候才能確定的令野,這個(gè)時(shí)候不能在棧上為其分配空間了,所以這個(gè)內(nèi)存是分配在堆('heap')空間上的徽级。
堆內(nèi)存涉及指針操作气破。不再贅述....說多了我就懵了。
靜態(tài)內(nèi)存分配和動(dòng)態(tài)內(nèi)存的區(qū)別:
-
靜態(tài)內(nèi)存分配:
- 編譯期知道所需內(nèi)存空間大小餐抢。
- 編譯期執(zhí)行
- 申請到椣质梗空間
- FILO(先進(jìn)后出)
-
動(dòng)態(tài)內(nèi)存分配:
- 編譯期不知道所需內(nèi)存空間大小
- 運(yùn)行期執(zhí)行
- 申請到堆空間
- 沒有特定的順序
總之說那么多,還不如一句話:
stack是采用靜態(tài)內(nèi)存分配的內(nèi)存空間旷痕,由系統(tǒng)自行釋放碳锈。heap是采用動(dòng)態(tài)內(nèi)存分配的內(nèi)存空間,無序欺抗,大小不定售碳,不會(huì)自動(dòng)釋放,哪怕你退出程序绞呈,那一塊內(nèi)存還是在那兒团滥。
堆('heap')
臥槽,前邊講多了报强,這里不知道說啥了灸姊。反正根據(jù)前邊說的動(dòng)態(tài)分配和靜態(tài)分配我們可以知道:
在JavaScript中,引用類型數(shù)據(jù)(對象秉溉、數(shù)組力惯、函數(shù))碗誉,這么說不太準(zhǔn)確,數(shù)組和函數(shù)也是對象父晶,就這么地吧哮缺。
它們都是申請到堆空間的,然后有一個(gè)引用甲喝,可以理解為一個(gè)指針尝苇,它保存了這個(gè)對象在堆中的位置。這個(gè)引用是存到棧中的埠胖。
棧('stack')
也叫堆棧糠溜。基本數(shù)據(jù)類型String直撤,Boolean之類的變量是申請到椃歉停空間的。
隊(duì)列('queue')
之前看過一個(gè)段子:
棧和隊(duì)列的區(qū)別? --吃多了拉就是隊(duì)列谋竖,吃多了吐就是棧红柱。
這特么也太有才了。不過說明了棧和隊(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是多少?
沒啥說的零聚,前者a,b都在棧空間申請了內(nèi)存众眨,var b=a的時(shí)候分配了新的值握牧。兩者互不相干容诬。
后邊的是引用傳遞娩梨,兩者指向堆內(nèi)存空間的某個(gè)位置的同一個(gè)對象。所以對對象的操作是互相影響的览徒。
深淺拷貝
淺拷貝:可以理解為只拷貝了1層狈定,如果有數(shù)組之類的對象的話,實(shí)際是拷貝了其引用习蓬。所以操作該對象是互相影響的纽什。內(nèi)存上是兩個(gè)引用指向了堆空間中的同一對象
var o = {
name: 'jack ma',
friends: ['李彥宏', '馬化騰']
}
var c = Object.assign({}, o);
c.friends.push('雷軍');
o.friends; // ["李彥宏", "馬化騰", "雷軍"]
深拷貝: 就是遞歸的拷貝,把屬性值也拷貝了躲叼÷郑互不影響了。內(nèi)存上是兩個(gè)引用分別指向了堆空間中的不同對象枫慷,但是初始值是一樣的让蕾。
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è)算法是最簡單的缸濒,假如一個(gè)對象沒有指針指向它,那它就被認(rèn)為是可回收的粱腻。
下面是MDN上面的例子:
var o = { a: { b:2 } }; // 兩個(gè)對象被創(chuàng)建庇配,一個(gè)作為另一個(gè)的屬性被引用,另一個(gè)被分配給變量o // 很顯然栖疑,沒有一個(gè)可以被垃圾收集 var o2 = o; // o2變量是第二個(gè)對“這個(gè)對象”的引用 o = 1; // 現(xiàn)在讨永,“這個(gè)對象”的原始引用o被o2替換了 var oa = o2.a; // 引用“這個(gè)對象”的a屬性 // 現(xiàn)在,“這個(gè)對象”有兩個(gè)引用了遇革,一個(gè)是o2卿闹,一個(gè)是oa o2 = "yo"; // 最初的對象現(xiàn)在已經(jīng)是零引用了 // 他可以被垃圾回收了 // 然而它的屬性a的對象還在被oa引用,所以還不能回收 oa = null; // a屬性的那個(gè)對象現(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)為對象至少會(huì)被引用一次萝快,而不會(huì)回收這塊內(nèi)存锻霎。導(dǎo)致內(nèi)存泄露。
-
標(biāo)記-清除算法
這個(gè)算法是現(xiàn)在瀏覽器基本都有的揪漩,其核心思想就是不能被引用的對象可被回收旋恼。
原理大致是:
- 有一個(gè)GC root列表,保存了引用的全局變量奄容,比如 "window".
- root被認(rèn)為是活動(dòng)的冰更,不被回收,然后遞歸檢查其子節(jié)點(diǎn)昂勒,可以被訪問的都標(biāo)記為活動(dòng)的蜀细。
- 所有的不被標(biāo)記的,都是可回收的戈盈。
[圖片上傳失敗...(image-9cf24e-1511490949760)]
這樣的話奠衔,上面的循環(huán)引用,在函數(shù)結(jié)束后塘娶,o1,o2不再被全局變量所能訪問的對象引用归斤。就會(huì)被認(rèn)為是垃圾
內(nèi)存泄露
首先GC是無法預(yù)測的,其實(shí)回收更多的是取決于我們自己怎么去寫程序刁岸≡嗬铮或多或少年少的我們寫的代碼都導(dǎo)致了一些內(nèi)存無法被釋放,造成了內(nèi)存的泄露虹曙。
常見的內(nèi)存泄露
以下都是copy的經(jīng)典例子迫横。
-
全局變量
根據(jù)上邊的標(biāo)記-清除算法鸦难,root列表中的全局變量是不會(huì)被釋放的。所以我們的代碼中顯式的全局或者隱式的全局變量是不會(huì)被垃圾收集器回收的员淫。
隱式的情況全局變量有(還有很多):
-
忘記寫聲明了合蔽。
function foo(){ boss = 'jack ma' } foo(); window.boss; // "jack ma"
引擎對boss進(jìn)行LHS查詢,在當(dāng)前作用域沒有找到聲明介返,就去外層也就是全局之中找拴事,也特么沒找到,這個(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ī)則牲证,也就說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ù)等太,它是沒辦法被收集的捂齐。但是現(xiàn)代瀏覽器在這個(gè)問題已經(jīng)做了優(yōu)化,無需擔(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è)作用域就可以被共享。
也就是說為someMethod創(chuàng)建的作用域是被unused共享的瞻想。theThing作為root持有對someMethod的引用压真,unused引用的originalThing,也迫使其不會(huì)被回收蘑险。
這個(gè)問題是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樹中對 image 的引用 document.body.removeChild(document.getElementById('image')); // 但是GC并不會(huì)回收漠其。因?yàn)閑lements還引用了呀嘴高! }
chrome工具進(jìn)行內(nèi)存分析
利用瀏覽器進(jìn)行內(nèi)存分析具體步驟請執(zhí)行點(diǎn)擊下面的參考最后兩個(gè)竿音。
我們以上邊的閉包為例:
還有各種size之類的我就不說了和屎。反正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é)語
擼主實(shí)力有限春瞬,高手歷來在民間柴信,希望廣提意見,補(bǔ)腎感激宽气。歡迎star随常,對我也是一種鼓勵(lì)潜沦。