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

每種編程語言都有它的內(nèi)存管理機(jī)制掀泳,比如C語言這樣的底層語言,有原生內(nèi)存管理接口芹壕,像malloc()動態(tài)的分配內(nèi)存空間free()釋放動態(tài)分配的內(nèi)存空間盒蟆。開發(fā)人員使用這些接口可以顯式分配和釋放操作系統(tǒng)的內(nèi)存鬼店。

JS作為一門高級語言网棍,JS并不像底層語言C那樣擁有對內(nèi)存操作的完全掌控。相對地妇智,JavaScript會在創(chuàng)建變量(對象滥玷、字符串)時(shí)自動分配內(nèi)存氏身,并在這些變量不被使用時(shí)自動釋放內(nèi)存,這個(gè)過程被稱為垃圾回收惑畴。

內(nèi)存生命周期

不管什么程序語言蛋欣,內(nèi)存生命周期基本是一致的:

  • 分配你所需要的內(nèi)存
  • 使用分配到的內(nèi)存(進(jìn)行讀、寫)
  • 不需要時(shí)將內(nèi)存進(jìn)行釋放

JS 內(nèi)存模型

JavaScript中的內(nèi)存分配是由js引擎完成的如贷,內(nèi)存空間分為兩種:棧內(nèi)存(stack) 與 堆內(nèi)存(heap), 而JavaScript的數(shù)據(jù)類型也分為兩大類陷虎, 分別是基本數(shù)據(jù)類型和引用數(shù)據(jù)類型,與兩種內(nèi)存空間相對應(yīng)杠袱。

基礎(chǔ)數(shù)據(jù)類型與棧內(nèi)存

JS中的基礎(chǔ)數(shù)據(jù)類型都有固定的大小尚猿,往往都保存在棧內(nèi)存中(閉包除外),由系統(tǒng)自動分配存儲空間楣富。我們可以直接操作保存在棧內(nèi)存空間的值凿掂,因此基礎(chǔ)數(shù)據(jù)類型都是按值訪問數(shù)據(jù),在棧內(nèi)存中的存儲與使用方式類似于數(shù)據(jù)結(jié)構(gòu)中的堆棧數(shù)據(jù)結(jié)構(gòu)纹蝴,遵循后進(jìn)先出的原則庄萎。

基礎(chǔ)數(shù)據(jù)類型:

Number、String塘安、Null糠涛、Boolean、Undefiend兼犯、Symbol(ES6新增)

簡單理解棧的存取方式脱羡,我們可以通過類比乒乓球盒子來分析。


這種乒乓球的存放方式與棧中存取數(shù)據(jù)的方式如出一轍免都。處于盒子中最頂層的乒乓球5锉罐,它一定是最后被放進(jìn)去,但可以最先被拿出來绕娘。而我們想要拿出底層的乒乓球1脓规,就必須將上面的4個(gè)乒乓球取出來,讓乒乓球1處于盒子頂層险领。這就是椙扔撸空間先進(jìn)后出,后進(jìn)先出的特點(diǎn)绢陌。

引用數(shù)據(jù)類型與堆內(nèi)存

JS的引用數(shù)據(jù)類型挨下,比如數(shù)組Array,它們值的大小是不固定的脐湾。引用數(shù)據(jù)類型的值是保存在堆內(nèi)存中的對象臭笆。JavaScript不允許直接訪問堆內(nèi)存中的位置,因此我們不能直接操作對象的堆內(nèi)存空間。在操作對象時(shí)愁铺,實(shí)際上是在操作對象的引用而不是實(shí)際的對象鹰霍。因此,引用類型的值都是按引用訪問的茵乱。這里的引用茂洒,我們可以粗淺地理解為保存在變量對象中的一個(gè)地址,該地址與堆內(nèi)存的實(shí)際值相關(guān)聯(lián)瓶竭。

特點(diǎn):不連續(xù)的內(nèi)存區(qū)域督勺,容量較大,讀取速度慢(因?yàn)橐玫刂吩诙阎薪锓。嗔艘淮沃修D(zhuǎn)玷氏,所以讀取速度自然會比棧要慢)。隨意讀取腋舌,類似于圖書館書架上的書盏触,喜歡哪本拿哪本。

熟知的引用數(shù)據(jù)類型:

Object块饺、Array赞辩、Date、RegExp授艰、Function 等辨嗽。

var a1 = 0;   // 變量對象
var a2 = 'this is string'; // 變量對象
var a3 = null; // 變量對象

var b = { m: 20 }; // 變量b存在于變量對象中,{m: 20} 作為對象存在于堆內(nèi)存中
var c = [1, 2, 3]; // 變量c存在于變量對象中淮腾,[1, 2, 3] 作為對象存在于堆內(nèi)存中
image.png

當(dāng)我們要訪問堆內(nèi)存中的引用數(shù)據(jù)類型時(shí)糟需,實(shí)際上我們首先是從變量對象中獲取了該對象的地址引用(或者地址指針),然后再從堆內(nèi)存中取得我們需要的數(shù)據(jù)谷朝。

接下來洲押,我們通過下面的例子來加深對JS內(nèi)存的理解

var a = 20;
var b = a;
b = 30;

var m = { a: 10, b: 20 };
var n = m;
n.a = 15; 
image.png

在變量對象中的數(shù)據(jù)發(fā)生復(fù)制行為時(shí),系統(tǒng)會自動為新的變量分配一個(gè)新值圆凰。var b = a執(zhí)行之后杈帐,a與b雖然值都等于20,但是他們其實(shí)已經(jīng)是相互獨(dú)立互不影響的值了专钉。具體如圖挑童。所以我們修改了b的值以后,a的值并不會發(fā)生變化跃须。


image.png

通過var n = m執(zhí)行一次復(fù)制引用類型的操作站叼。引用類型的復(fù)制同樣也會為新的變量自動分配一個(gè)新的值保存在變量對象中,但不同的是菇民,這個(gè)新的值尽楔,僅僅只是引用類型的一個(gè)地址指針投储。當(dāng)?shù)刂分羔樝嗤瑫r(shí),盡管他們相互獨(dú)立翔试,但是在變量對象中訪問到的具體對象實(shí)際上是同一個(gè)轻要。

內(nèi)存回收

垃圾回收是一種內(nèi)存管理機(jī)制复旬,就是將不再用到的內(nèi)存及時(shí)釋放垦缅,以防內(nèi)存占用越來越高,防止卡頓甚至進(jìn)程崩潰驹碍。在JavaScript中有自動化的垃圾回收機(jī)制壁涎,自動回收過期無效的變量。

在JavaScript中內(nèi)存垃圾回收是由js引擎自動完成的志秃。實(shí)現(xiàn)垃圾回收的關(guān)鍵在于如何確定內(nèi)存不再使用怔球,也就是確定對象是否無用。主要有兩種方式:*引用計(jì)數(shù)標(biāo)記清除浮还。

引用計(jì)數(shù)算法

引用就是指向某一地址的指針竟坛。我們可簡單將引用視為一個(gè)對象訪問另一個(gè)對象的路徑。(這里的對象是一個(gè)寬泛的概念钧舌,泛指JS環(huán)境中的實(shí)體)担汤。

引用計(jì)數(shù)算法定義就是以內(nèi)存不再使用為標(biāo)準(zhǔn),就是看一個(gè)對象是否有指向它的引用洼冻。如果沒有其他地址指向它了崭歧,說明該對象已經(jīng)不再需要了,可以進(jìn)行回收撞牢。

下面來看個(gè)例子:

// 創(chuàng)建一個(gè)對象person率碾,他有兩個(gè)指向?qū)傩詀ge和name的引用
var person = {
    age: 22,
    name: 'ifcode'
};

person.name = null; // 雖然設(shè)置為null,但因?yàn)閜erson對象還有指向name的引用屋彪,因此name不會回收

var p = person; 
person = 1;         //原來的person對象被賦值為1所宰,但因?yàn)橛行乱胮指向原person對象,因此它不會被回收

p = null;           //原person對象已經(jīng)沒有引用畜挥,很快會被回收

由上面例子可以看出歧匈,引用計(jì)數(shù)算法是個(gè)簡單有效的算法。但它卻存在一個(gè)致命的問題:循環(huán)引用砰嘁。如果兩個(gè)對象相互引用件炉,盡管他們已不再使用,垃圾回收器不會進(jìn)行回收矮湘,導(dǎo)致內(nèi)存泄露斟冕。

function cycle() {
    var o1 = {};
    var o2 = {};
    o1.a = o2;
    o2.a = o1; 
    
    return "Cycle reference!"
}

cycle();

上面我們申明了一個(gè)cycle方程,其中包含兩個(gè)相互引用的對象缅阳。在調(diào)用函數(shù)結(jié)束后磕蛇,對象o1和o2實(shí)際上已離開函數(shù)范圍景描,因此不再需要了。但根據(jù)引用計(jì)數(shù)的原則秀撇,他們之間的相互引用依然存在超棺,因此這部分內(nèi)存不會被回收,內(nèi)存泄露不可避免了呵燕。

正是因?yàn)橛羞@個(gè)嚴(yán)重的缺點(diǎn),這個(gè)算法在現(xiàn)代瀏覽器中已經(jīng)被下面要介紹的標(biāo)記清除算法所取代了再扭。但絕不可認(rèn)為該問題已經(jīng)不再存在了,因?yàn)檫€占有大量市場的IE6泛范、IE7使用的正是這一算法。在需要照顧兼容性的時(shí)候罢荡,某些看起來非常普通的寫法也可能造成意想不到的問題:

var div = document.createElement("div");
div.onclick = function() {
    console.log("click");
};

現(xiàn)在雖然有各種框架,很少直接操作dom了 ,但上面這種JS寫法很簡單卻存在問題区赵。創(chuàng)建一個(gè)DOM元素并綁定一個(gè)點(diǎn)擊事件惭缰,這里有什么問題呢?請注意惧笛,變量div有事件處理函數(shù)的引用从媚,同時(shí)事件處理函數(shù)也有div的引用!(div變量可在函數(shù)內(nèi)被訪問)患整。一個(gè)循序引用出現(xiàn)了拜效,按上面所講的算法,該部分內(nèi)存無可避免地泄露了各谚。

標(biāo)記清除算法

上面說過紧憾,現(xiàn)代的瀏覽器已經(jīng)不再使用引用計(jì)數(shù)算法了。現(xiàn)代瀏覽器通用的大多是基于標(biāo)記清除算法的某些改進(jìn)算法昌渤,總體思想都是一致的赴穗。

標(biāo)記清除算法將“不再使用的對象”定義為“無法達(dá)到的對象”。簡單來說膀息,就是從根部(在JS中就是全局對象)出發(fā)定時(shí)掃描內(nèi)存中的對象般眉。凡是能從根部到達(dá)的對象,都是還需要使用的潜支。那些無法由根部出發(fā)觸及到的對象被標(biāo)記為不再使用甸赃,稍后進(jìn)行回收。

從這個(gè)概念可以看出冗酿,無法觸及的對象包含了沒有引用的對象這個(gè)概念(沒有任何引用的對象也是無法觸及的對象)埠对。但反之未必成立络断。

根據(jù)這個(gè)概念,上面的例子可以正確被垃圾回收處理了项玛。當(dāng)div與其時(shí)間處理函數(shù)不能再從全局對象出發(fā)觸及的時(shí)候貌笨,垃圾回收器就會標(biāo)記并回收這兩個(gè)對象。

image.png

內(nèi)存管理友好的JS代碼

如果還需要兼容老舊瀏覽器襟沮,那么就需要注意代碼中的循環(huán)引用問題锥惋。或者直接采用保證兼容性的庫來幫助優(yōu)化代碼臣嚣。
對現(xiàn)代瀏覽器來說净刮,唯一要注意的就是明確切斷需要回收的對象與根部的聯(lián)系剥哑。有時(shí)候這種聯(lián)系并不明顯硅则,且因?yàn)闃?biāo)記清除算法的強(qiáng)壯性,這個(gè)問題較少出現(xiàn)株婴。最常見的內(nèi)存泄露一般都與DOM元素綁定有關(guān):

email.message = document.createElement(“div”);
displayList.appendChild(email.message);

// 稍后從displayList中清除DOM元素
displayList.removeAllChildren();

div元素已經(jīng)從DOM樹中清除,也就是說從DOM樹的根部無法觸及該div元素了大审。但是請注意徒扶,div元素同時(shí)也綁定了email對象。所以只要email對象還存在姜骡,該div元素將一直保存在內(nèi)存中屿良。

內(nèi)存泄露

對于持續(xù)運(yùn)行的服務(wù)進(jìn)程(daemon)尘惧,必須及時(shí)釋放不再用到的內(nèi)存。否則啥么,內(nèi)存占用越來越高悬荣,輕則影響系統(tǒng)性能似踱,重則導(dǎo)致進(jìn)程崩潰稽煤。 不再用到的內(nèi)存酵熙,沒有及時(shí)釋放匾二,就叫做內(nèi)存泄漏拳芙。

常見的內(nèi)存泄露

1.意外的全局變量
function foo() {
      bar = '全局變量'; // 沒有聲明變量 實(shí)際上是全局變量=>window.bar
    }
    foo();


function foo() {
     const bar = 'foo變量'; // 沒有聲明變量 實(shí)際上是全局變量=>window.bar
    }
    foo();
2.定時(shí)器和回調(diào)函數(shù)

當(dāng)不需要setInterval或者setTimeout時(shí)分飞,定時(shí)器沒有被清除譬猫,定時(shí)器的回調(diào)函數(shù)以及內(nèi)部依賴的變量都不能被回收,造成內(nèi)存泄漏染服。

setInterval(function() {
    // 執(zhí)行什么
}, 1000);

頁面卸載或者執(zhí)行完定時(shí)器需要主動清除

3.濫用閉包
function fn2(){
  let test = new Array(1000).fill('test')
  return function(){
    console.log(test)
    return test
  }
}
let fn2Child = fn2()
fn2Child()
//fn2Child = null 解決方法 函數(shù)調(diào)用后柳刮,把外部的引用關(guān)系置空

return 的函數(shù)中存在函數(shù) fn2 中的 test 變量引用秉颗,所以 test 并不會被回收站宗,也就造成了內(nèi)存泄漏梢灭。fn2Child = null 解決方法 函數(shù)調(diào)用后蒸其,把外部的引用關(guān)系置空

4.沒有清理DOM元素引用
var refA = document.getElementById("test");
document.body.removeChild(refA); // dom刪除了
console.log(refA, "refA");  // 但是還存在引用 能console出整個(gè)div 沒有被回收
refA = null;
console.log(refA, "refA");  // 解除引用
5.console保存大量數(shù)據(jù)在內(nèi)存中

過多的console摸袁,比如定時(shí)器的console會導(dǎo)致瀏覽器卡死靠汁。
合理利用console闽铐,線上項(xiàng)目盡量少的使用console兄墅。

內(nèi)存查看

  • 瀏覽器方法
    1.打開Chrome瀏覽器開發(fā)者工具的Performance面板
    2.選項(xiàng)欄中勾選Memory選項(xiàng)
    3.點(diǎn)擊左上角錄制按鈕(實(shí)心圓狀按鈕)
    4.在頁面上進(jìn)行正常操作
    5.一段時(shí)間后隙咸,點(diǎn)擊Stop五督,觀察面板上的數(shù)據(jù)


    image.png

如果內(nèi)存占用基本平穩(wěn)充包,接近水平误证,就說明不存在內(nèi)存泄漏修壕。

image

反之遏考,就是內(nèi)存泄漏了灌具。

image
  • 命令行方法
    命令行可以使用 Node 提供的 process.memoryUsage 方法咖楣。
console.log(process.memoryUsage());
//{
  //rss: 101568512,
  //heapTotal: 72605696,
  //heapUsed: 51070584,
  //external: 5819790,
  //arrayBuffers: 4286309
//}

process.memoryUsage返回一個(gè)對象诱贿,包含了 Node 進(jìn)程的內(nèi)存占用信息珠十。該對象包含四個(gè)字段,單位是字節(jié)晒杈,含義如下:

rss(resident set size):所有內(nèi)存占用拯钻,包括指令區(qū)和堆棧。
heapTotal:"堆"占用的內(nèi)存然磷,包括用到的和沒用到的姿搜。
heapUsed:用到的堆的部分舅柜。
external: V8 引擎內(nèi)部的 C++ 對象占用的內(nèi)存致份。
arrayBuffers: ArrayBufferSharedArrayBuffer 分配的內(nèi)存氮块,包括所有 Node.js Buffer

判斷內(nèi)存泄漏滔蝉,以heapUsed字段為準(zhǔn)塔沃。

參考:
https://juejin.cn/post/6844903801191661575#heading-13
http://www.ruanyifeng.com/blog/2017/04/memory-leak.html
https://juejin.cn/post/6844903801191661575#heading-9

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末螃概,一起剝皮案震驚了整個(gè)濱河市吊洼,隨后出現(xiàn)的幾起案子冒窍,更是在濱河造成了極大的恐慌,老刑警劉巖超燃,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件意乓,死亡現(xiàn)場離奇詭異笆凌,居然都是意外死亡乞而,警方通過查閱死者的電腦和手機(jī)慢显,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屋灌,“玉大人共郭,你說我怎么就攤上這事疾呻“段希” “怎么了尉咕?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵龙考,是天一觀的道長炎功。 經(jīng)常有香客問我,道長蛇损,這世上最難降的妖魔是什么赁温? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮淤齐,結(jié)果婚禮上股囊,老公的妹妹穿的比我還像新娘。我一直安慰自己更啄,他們只是感情好稚疹,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著祭务,像睡著了一般内狗。 火紅的嫁衣襯著肌膚如雪怪嫌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天柳沙,我揣著相機(jī)與錄音赂鲤,去河邊找鬼。 笑死缴允,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鞠抑,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤端幼,失蹤者是張志新(化名)和其女友劉穎庭呜,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡秸架,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年食茎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡管宵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布称鳞,位于F島的核電站,受9級特大地震影響怨咪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜巍膘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一荚恶、第九天 我趴在偏房一處隱蔽的房頂上張望廓潜。 院中可真熱鬧,春花似錦味滞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間奸绷,已是汗流浹背健盒。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留莱衩,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓匾七,卻偏偏與公主長得像邑贴,于是被迫代替她去往敵國和親席里。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 簡介每種編程語言都有它的內(nèi)存管理機(jī)制拢驾,比如簡單的C有低級的內(nèi)存管理基元奖磁,像malloc(),free()。同樣我們...
    曲昶光閱讀 199評論 0 1
  • 前言 每種編程語言都有它的內(nèi)存管理機(jī)制繁疤,比如簡單的C有低級的內(nèi)存管理基元咖为,像malloc(),free()秕狰。而對于...
    青城墨闋閱讀 979評論 0 0
  • 為什么要關(guān)注內(nèi)存 任何程序的運(yùn)行都要分配運(yùn)行空間。 如果不在使用的內(nèi)容得不到釋放躁染,不會返回到操作系統(tǒng)或空閑內(nèi)存池鸣哀,...
    夏末遠(yuǎn)歌閱讀 317評論 0 0
  • 對于前端攻城師來說,JS的內(nèi)存機(jī)制不容忽視吞彤。如果想成為行業(yè)專家我衬,或者打造高性能前端應(yīng)用,那就必須要弄清楚JavaS...
    IT沐華閱讀 441評論 0 0
  • JavaScript不同于其他語言饰恕,在JavaScript中的內(nèi)存都是自動分配和回收挠羔。如同請人打掃衛(wèi)生。其實(shí)在大多...
    Pamcore閱讀 170評論 0 0