Javascript內(nèi)存機(jī)制

為什么要關(guān)注內(nèi)存

  1. 任何程序的運(yùn)行都要分配運(yùn)行空間。
  2. 如果不在使用的內(nèi)容得不到釋放,不會(huì)返回到操作系統(tǒng)或空閑內(nèi)存池,會(huì)導(dǎo)致內(nèi)存泄露。
  3. 程序運(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編譯原理:

  1. var a ,編譯器判斷當(dāng)前作用域中是否已存在該變量,如果有,則忽略;否則在當(dāng)前作用域中新聲明一個(gè)變量,命名為 a
  2. a = 2,引擎運(yùn)行時(shí),先判斷作用域中是否存在 變量 a。如果存在變量 a,進(jìn)行賦值操作,將2賦值給a;否則拋出異常。

當(dāng)聲明變量a并初始化值為10時(shí)

  1. 為變量a創(chuàng)建為標(biāo)識(shí)符

  2. 在棧中分配地址,指向標(biāo)識(shí)符

  3. 將值10存儲(chǔ)在標(biāo)識(shí)符對(duì)應(yīng)的地址

    也就是值傳遞。

1583419546970.png

聲明變量b,然后賦值時(shí):

  1. 為變量b創(chuàng)建標(biāo)識(shí)符
  2. 將變量a在棧中的地址,指向b。
1583420078960.png

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.

1583420312259.png

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

1583420997669.png
基本數(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
}
  1. 為變量創(chuàng)建標(biāo)識(shí)符a
  2. 在棧中分配地址,指向標(biāo)識(shí)符
  3. 在堆內(nèi)存中分配空間
  4. 在棧中存儲(chǔ)堆內(nèi)存的存儲(chǔ)地址
1583422758145.png

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

1583424712387.png

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ì)象 oo2并且相互引用,形成了一個(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)存管理

弊病

  1. 為瀏覽器設(shè)計(jì),不太可能遇到大量內(nèi)存的場景,64位下 新生代默認(rèn)的最大內(nèi)存空間為32MB,老生代默認(rèn)的最大內(nèi)存空間為1400MB。
  2. 垃圾回收會(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)行垃圾回收。

1583434994243.png

內(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):

  1. 該對(duì)象已經(jīng)進(jìn)行過一次Scavenge回收;

  2. To空間已使用了25%。

![1583435310890.png](https://upload-images.jianshu.io/upload_images/6366468-78c5ffa23e1d0520.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

老生代

對(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)存

1583435431583.png

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

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