為什么要關(guān)注內(nèi)存
- 任何程序的運(yùn)行都要分配運(yùn)行空間。
- 如果不在使用的內(nèi)容得不到釋放,不會(huì)返回到操作系統(tǒng)或空閑內(nèi)存池,會(huì)導(dǎo)致內(nèi)存泄露。
- 程序運(yùn)行所需的內(nèi)存空間大于當(dāng)前的可用內(nèi)存空間會(huì)引發(fā)內(nèi)存溢出。
JS數(shù)據(jù)類型與JS內(nèi)存機(jī)制
數(shù)據(jù)類型
原始數(shù)據(jù)類型:
- 字符串 string
- 數(shù)字 number
- 布爾 boolean
- 空對(duì)象 null
- 未定義 undefined
引用數(shù)據(jù)類型:
- object
- function
- array
內(nèi)置對(duì)象(實(shí)際上是內(nèi)置函數(shù),可以當(dāng)做構(gòu)造器使用)
- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
內(nèi)存空間:
- 棧 stack 存放原始數(shù)據(jù)類型
- 堆 heap 存放引用數(shù)據(jù)類型( Array、Object、Function)
棧
棧,一種數(shù)據(jù)結(jié)構(gòu),限定在表尾進(jìn)行插入和刪除操作的線性表。
特點(diǎn):后進(jìn)先出(Last In First Out)–LIFO
特別的是,允許插入和刪除的一端稱為棧頂,另一端稱為棧底。
棧的插入操作,叫進(jìn)棧、入?;驂簵?。
棧的刪除操作,叫出棧、或彈棧。
可以想象成彈夾壓子彈,1-2-3 入彈夾,3-2-1 出彈夾。
var a = 10;
var b;
b=a;
Javascript編譯原理:
var a,編譯器判斷當(dāng)前作用域中是否已存在該變量,如果有,則忽略;否則在當(dāng)前作用域中新聲明一個(gè)變量,命名為aa = 2,引擎運(yùn)行時(shí),先判斷作用域中是否存在 變量 a。如果存在變量 a,進(jìn)行賦值操作,將2賦值給a;否則拋出異常。
當(dāng)聲明變量a并初始化值為10時(shí)
為變量a創(chuàng)建為標(biāo)識(shí)符
在棧中分配地址,指向標(biāo)識(shí)符
-
將值10存儲(chǔ)在標(biāo)識(shí)符對(duì)應(yīng)的地址
也就是值傳遞。

聲明變量b,然后賦值時(shí):
- 為變量b創(chuàng)建標(biāo)識(shí)符
- 將變量a在棧中的地址,指向b。

a==b,結(jié)果是什么?true因?yàn)閍,b均為原始數(shù)據(jù)引用,在比較值的時(shí)候,比較的是值的本身。
如果此時(shí)我們執(zhí)行a=true,棧中會(huì)發(fā)生什么變化呢
因?yàn)闂V写嬖诘氖窃紨?shù)據(jù)類型,其不可變,當(dāng)我們將賦值true時(shí),將在棧中新分配地址,并指向a,同時(shí)b的值指向不變,仍為10.

再次操作b=null后,新分配內(nèi)存空間值為null,由于,地址為0,值為10的內(nèi)存未關(guān)聯(lián)任何變量,會(huì)被垃圾回收釋放此空間。

基本數(shù)據(jù)類型存在堆的情況
閉包:將內(nèi)部函數(shù)傳遞到所在的詞法作用域以外,都會(huì)持有對(duì)原始定義作用域的引用。
當(dāng)一個(gè)基本類型被閉包引用之后,就可以長期存在于內(nèi)存中,這個(gè)時(shí)候即使他是基本類型,也是會(huì)被存放在堆中的。
function foo(){
var name='bob';
return function (){
console.log(name)
}
}
var bar=foo();
bar();//bob
正常情況下,foo在執(zhí)行完成后,會(huì)被垃圾回收器掉,但是因?yàn)殚]包的存在,內(nèi)部函數(shù)仍保留著局部變量name的引用,導(dǎo)致內(nèi)存無法釋放,所以不能濫用閉包。需要及時(shí)將退出函數(shù)前,將閉包內(nèi)的變量引用刪除。
堆
是存儲(chǔ)引用類型的地方。跟調(diào)用堆棧主要的區(qū)別在于,堆可以存儲(chǔ)無序的數(shù)據(jù),這些數(shù)據(jù)可以動(dòng)態(tài)地增長,非常適合數(shù)組和對(duì)象。在Javascript中我們無法直接操作堆,我們在操作對(duì)象時(shí),實(shí)際是在操作對(duì)象的引用。
當(dāng)如下聲明時(shí):
var a={
name:'Bob',
age:18
}
- 為變量創(chuàng)建標(biāo)識(shí)符
a - 在棧中分配地址,指向標(biāo)識(shí)符
- 在堆內(nèi)存中分配空間
- 在棧中存儲(chǔ)堆內(nèi)存的存儲(chǔ)地址

那如果我將一個(gè)對(duì)象賦值給另一個(gè)變量呢?var b=a ,棧中會(huì)配一個(gè)新的值,來存放新的變量,但是這兩個(gè)變量的地址是一樣的,相當(dāng)于指向的對(duì)象是一樣的

a.name=‘Tom’這里只是修改了堆內(nèi)存地址0x1021中的數(shù)據(jù),并未修改變量a的指向的內(nèi)存地址。又因?yàn)樽兞縜和b指向了內(nèi)存空間的同一個(gè)地址,所有b.name也等于Tom
對(duì)象屬性的內(nèi)存模型
不同于原始數(shù)據(jù)內(nèi)存模型,一個(gè)對(duì)象可以包含多個(gè)屬性,而對(duì)象的屬性又可以分為原始數(shù)據(jù)和引用數(shù)據(jù)。
var obj = {
name:'Bob',
age:'18',
behaviour:{
fly:function(){
console.log("can fly")
},
eat:{
noodles:'大碗寬面'
}
}
}
并不是說obj變量為引用類型,在堆內(nèi)存中直接存放了。
obj來說,變量obj指向了堆內(nèi)存中分配給引用數(shù)據(jù)對(duì)象的地址。從obj的屬性來看,屬性只是指向了屬性值的內(nèi)存地址,并不指向?qū)嶋H的對(duì)象。也就是說對(duì)象的屬性指向的也是引用,指向這些值真正存放的地方。
垃圾回收
Javascript在創(chuàng)建變量時(shí)(對(duì)象、字符串等)時(shí)會(huì)自動(dòng)分配內(nèi)存, 并且在不使用他們時(shí)釋放 。
優(yōu)勢:由引擎跟蹤內(nèi)存的分配和使用,以便當(dāng)分配的內(nèi)存不再使用時(shí),自動(dòng)釋放它,減少內(nèi)存空間不足帶來的內(nèi)存泄露。
劣勢:未提供相應(yīng)的api,無法人為進(jìn)行內(nèi)存操作。
垃圾回收算法主要依賴 引用 (在內(nèi)存管理的環(huán)境中,一個(gè)對(duì)象如果有訪問另一個(gè)對(duì)象的權(quán)限(隱式或者顯式),叫做一個(gè)對(duì)象引用另一個(gè)對(duì)象)
引用計(jì)數(shù)法
記錄每個(gè)值被引用的次數(shù),當(dāng)引用數(shù)為0時(shí),表示這個(gè)值不再使用了,判定可以進(jìn)行釋放。
var o = {
a: {
b:2
}
};
// 兩個(gè)對(duì)象被創(chuàng)建,一個(gè)作為另一個(gè)的屬性被引用,另一個(gè)被分配給變量o
// 很顯然,沒有一個(gè)可以被垃圾收集
var o2 = o; // o2變量是第二個(gè)對(duì)“這個(gè)對(duì)象”的引用
o = 1; // 現(xiàn)在,“這個(gè)對(duì)象”只有一個(gè)o2變量的引用了,“這個(gè)對(duì)象”的原始引用o已經(jīng)沒有
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)在也是零引用了
// 它可以被垃圾回收了
弊端(IE8及以下)
我們來看個(gè)例子
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
}
f();
這里創(chuàng)建了兩個(gè)對(duì)象 o和o2并且相互引用,形成了一個(gè)循環(huán)。當(dāng)函數(shù)f執(zhí)行完成后,內(nèi)部作用域銷毀,我們期待垃圾回收機(jī)制幫助我們銷毀這兩個(gè)對(duì)象并回收對(duì)應(yīng)的空間,但是兩個(gè)對(duì)象之間都保留有一次引用。
如果出現(xiàn)循環(huán)引用,那么值所占的空間將用永遠(yuǎn)得不到釋放,運(yùn)行時(shí)間越長,越容易引擎內(nèi)存泄露。
小tip:可以使用JSON.stringfy(o)來檢測對(duì)象是否存在循環(huán)引用。
標(biāo)記清除法(2012年起,所有瀏覽器均使用了此機(jī)制)
主要依賴與計(jì)算環(huán)境
執(zhí)行環(huán)境:定義了變量或函數(shù)有權(quán)訪問的其他數(shù)據(jù),決定了他們各自的行為。每個(gè)執(zhí)行環(huán)境都有一個(gè)與之相關(guān)聯(lián)的變量對(duì)象(全局對(duì)象/局部對(duì)象),環(huán)境中定義的所有變量和函數(shù)都保存在這個(gè)對(duì)象中。
當(dāng)變量進(jìn)入執(zhí)行環(huán)境時(shí),就標(biāo)記這個(gè)變量為“進(jìn)入環(huán)境”。從邏輯上講,永遠(yuǎn)不能釋放進(jìn)入環(huán)境的變量所占用的內(nèi)存,因?yàn)橹灰獔?zhí)行流進(jìn)入相應(yīng)的環(huán)境,就可能會(huì)用到他們。當(dāng)變量離開環(huán)境時(shí),則將其標(biāo)記為“離開環(huán)境”。
垃圾收集器在運(yùn)行的時(shí)候會(huì)給存儲(chǔ)在內(nèi)存中的所有變量都加上標(biāo)記。然后,它會(huì)去掉環(huán)境中的變量以及被環(huán)境中的變量引用的標(biāo)記。而在此之后再被加上標(biāo)記的變量將被視為準(zhǔn)備刪除的變量,原因是環(huán)境中的變量已經(jīng)無法訪問到這些變量了。最后。垃圾收集器完成內(nèi)存清除工作,銷毀那些帶標(biāo)記的值,并回收他們所占用的內(nèi)存空間。
簡單理解為:當(dāng)每個(gè)變量或函數(shù)在作用域鏈中無法訪問,那么就該收集了。
目前主流瀏覽器都是使用標(biāo)記清除式的垃圾回收策略,只不過收集的間隔有所不同
V8內(nèi)存管理
弊病
- 為瀏覽器設(shè)計(jì),不太可能遇到大量內(nèi)存的場景,64位下 新生代默認(rèn)的最大內(nèi)存空間為32MB,老生代默認(rèn)的最大內(nèi)存空間為1400MB。
- 垃圾回收會(huì)導(dǎo)致線程短暫停止線程從而引起性能問題。
回收策略:分代式垃圾回收機(jī)制
新生代:大多數(shù)對(duì)象被分配在這里。新生區(qū)是一個(gè)很小的區(qū)域,垃圾回收在這個(gè)區(qū)域非常頻繁,與其他區(qū)域相獨(dú)立
老生代:這里包含大多數(shù)可能存在指向其他對(duì)象的指針的對(duì)象。大多數(shù)在新生區(qū)存活一段時(shí)間之后的對(duì)象都會(huì)被挪到這里
回收算法
新生代
新生代中的對(duì)象主要通過Scavenge算法進(jìn)行垃圾回收。

內(nèi)存分配空間時(shí),分為兩個(gè)區(qū)域:From空間和To空間。
- 當(dāng)分配新的對(duì)象時(shí),總是往From空間中分配。
- 在回收時(shí),先掃描From空間,將From空間中存活的對(duì)象復(fù)制到To空間中,然后將From空間的內(nèi)存全部釋放,最后將From和To的角色交換
特點(diǎn):
只能使用一半的內(nèi)存,但由于只需要復(fù)制存活對(duì)象,因此該算法非常適合應(yīng)用在新生代垃圾回收中,因?yàn)樾律袑?duì)象的生命周期較短,垃圾回收時(shí)多為未存活對(duì)象。
不會(huì)在內(nèi)存中留下碎片
對(duì)象晉升
在執(zhí)行Scavenge的存活對(duì)象復(fù)制操作時(shí)進(jìn)行對(duì)象是否晉升的判斷(新生代遷移至老生代)
晉升標(biāo)準(zhǔn):
該對(duì)象已經(jīng)進(jìn)行過一次Scavenge回收;
To空間已使用了25%。

老生代
對(duì)于老生代中的對(duì)象,由于存活對(duì)象占較大比重,再采用Scavenge的方式會(huì)有兩個(gè)問題:
一是存活對(duì)象較多,復(fù)制存活對(duì)象的效率將會(huì)很低;
另一個(gè)問題則是由于老生代空間較大,空閑一半空間的做法對(duì)內(nèi)存是極大的浪費(fèi)
主要采用了Mark-Sweep和Mark-Compact兩種算法相結(jié)合的方式進(jìn)行垃圾回收。
Mark-Sweep
分為標(biāo)記階段和清除階段:
- 標(biāo)記階段會(huì)遍歷老生代空間的所有對(duì)象,將其中非存活的對(duì)象標(biāo)記出來;
- 清除階段則會(huì)將標(biāo)記的死亡對(duì)象一一清除,釋放內(nèi)存空間。
缺點(diǎn):回收后會(huì)在內(nèi)存中留下一些碎片,如果這時(shí)候需要分配大對(duì)象,不連續(xù)的內(nèi)存可能無法滿足需求
Mark-Compact
分為標(biāo)記和合并階段:
標(biāo)記階段會(huì)遍歷老生代空間的所有對(duì)象,將其中非存活的對(duì)象標(biāo)記出來;
合并階段會(huì)將活著的對(duì)象往一端移動(dòng),移動(dòng)完成后,直接清理掉邊界外的內(nèi)存

算法對(duì)比
| 回收算法 | Mark-Sweep | Mark-Compact | Scavenge |
|---|---|---|---|
| 速度 | 中等 | 最慢 | 最快 |
| 空間開銷 | 少(有碎片) | 少(無碎片) | 雙倍空間(無碎片) |
| 是否移動(dòng)對(duì)象 | 否 | 是 | 是 |
| 主動(dòng)啟動(dòng)時(shí)機(jī) | 進(jìn)程空閑時(shí) | 進(jìn)程空閑時(shí) | 進(jìn)程空閑時(shí)(頻率低) |
| 被動(dòng)啟動(dòng)時(shí)機(jī) | 1.老生代空間中被分配了一定數(shù)量的對(duì)象的時(shí)候; 2.老生代空間里沒有新生代空間大小相同的空間的時(shí)候 |
老生代空間的碎片到達(dá)一定數(shù)量的時(shí)候 | From空間沒有足夠的空間分配對(duì)象 |