您好,登錄后才能下訂單哦!
這篇文章主要介紹了javascript中內(nèi)存指的是什么的相關(guān)知識(shí),內(nèi)容詳細(xì)易懂,操作簡(jiǎn)單快捷,具有一定借鑒價(jià)值,相信大家閱讀完這篇javascript中內(nèi)存指的是什么文章都會(huì)有所收獲,下面我們一起來(lái)看看吧。
在javascript中,內(nèi)存通常指的是操作系統(tǒng)從主存中劃分(抽象)出來(lái)的內(nèi)存空間。內(nèi)存可分為兩類:1、棧內(nèi)存,是一段連續(xù)的內(nèi)存空間,容量較小,主要用于存放函數(shù)調(diào)用信息和變量等數(shù)據(jù),大量的內(nèi)存分配操作會(huì)導(dǎo)致棧溢出;2、堆內(nèi)存,是一大片內(nèi)存空間,堆內(nèi)存的分配是動(dòng)態(tài)且不連續(xù)的,程序可以按需申請(qǐng)堆內(nèi)存空間,但是訪問(wèn)速度要比棧內(nèi)存慢不少。
本教程操作環(huán)境:windows7系統(tǒng)、javascript1.8.5版、Dell G3電腦。
JavaScript 誕生于 1995 年,最初被設(shè)計(jì)用于網(wǎng)頁(yè)內(nèi)的表單驗(yàn)證。
這些年來(lái) JavaScript 成長(zhǎng)飛速,生態(tài)圈日益壯大,成為了最受程序員歡迎的開(kāi)發(fā)語(yǔ)言之一。并且現(xiàn)在的 JavaScript 不再局限于網(wǎng)頁(yè)端,已經(jīng)擴(kuò)展到了桌面端、移動(dòng)端以及服務(wù)端。
隨著大前端時(shí)代的到來(lái),使用 JavaScript 的開(kāi)發(fā)者越來(lái)越多,但是許多開(kāi)發(fā)者都只停留在“會(huì)用”這個(gè)層面,而對(duì)于這門語(yǔ)言并沒(méi)有更多的了解。
如果想要成為一名更好的 JavaScript 開(kāi)發(fā)者,理解內(nèi)存是一個(gè)不可忽略的關(guān)鍵點(diǎn)。
本文主要包含兩大部分:
JavaScript 內(nèi)存詳解
JavaScript 內(nèi)存分析指南
相信大家都對(duì)內(nèi)存有一定的了解,我就不從盤古開(kāi)天辟地開(kāi)始講了,稍微提一下。
首先,任何應(yīng)用程序想要運(yùn)行都離不開(kāi)內(nèi)存。
另外,我們提到的內(nèi)存在不同的層面上有著不同的含義。
硬件層面(Hardware)
在硬件層面上,內(nèi)存指的是隨機(jī)存取存儲(chǔ)器。
內(nèi)存是計(jì)算機(jī)重要組成部分,用來(lái)儲(chǔ)存應(yīng)用運(yùn)行所需要的各種數(shù)據(jù),CPU 能夠直接與內(nèi)存交換數(shù)據(jù),保證應(yīng)用能夠流暢運(yùn)行。
一般來(lái)說(shuō),在計(jì)算機(jī)的組成中主要有兩種隨機(jī)存取存儲(chǔ)器:高速緩存(Cache)和主存儲(chǔ)器(Main memory)。
高速緩存通常直接集成在 CPU 內(nèi)部,離我們比較遠(yuǎn),所以更多時(shí)候我們提到的(硬件)內(nèi)存都是主存儲(chǔ)器。
? 隨機(jī)存取存儲(chǔ)器(Random Access Memory,RAM)
隨機(jī)存取存儲(chǔ)器分為靜態(tài)隨機(jī)存取存儲(chǔ)器(Static Random Access Memory,SRAM)和動(dòng)態(tài)隨機(jī)存取存儲(chǔ)器(Dynamic Random Access Memory,DRAM)兩大類。
在速度上 SRAM 要遠(yuǎn)快于 DRAM,而 SRAM 的速度僅次于 CPU 內(nèi)部的寄存器。
在現(xiàn)代計(jì)算機(jī)中,高速緩存使用的是 SRAM,而主存儲(chǔ)器使用的是 DRAM。
? 主存儲(chǔ)器(Main memory,主存)
雖然高速緩存的速度很快,但是其存儲(chǔ)容量很小,小到幾 KB 最大也才幾十 MB,根本不足以儲(chǔ)存應(yīng)用運(yùn)行的數(shù)據(jù)。
我們需要一種存儲(chǔ)容量與速度適中的存儲(chǔ)部件,讓我們?cè)诒WC性能的情況下,能夠同時(shí)運(yùn)行幾十甚至上百個(gè)應(yīng)用,這也就是主存的作用。
計(jì)算機(jī)中的主存其實(shí)就是我們平時(shí)說(shuō)的內(nèi)存條(硬件)。
硬件內(nèi)存不是我們今天的主題,所以就說(shuō)這么多,想要深入了解的話可以根據(jù)上面提到關(guān)鍵詞進(jìn)行搜索。
軟件層面(Software)
在軟件層面上,內(nèi)存通常指的是操作系統(tǒng)從主存中劃分(抽象)出來(lái)的內(nèi)存空間。
此時(shí)內(nèi)存又可以分為兩類:棧內(nèi)存和堆內(nèi)存。
接下來(lái)我將圍繞 JavaScript 這門語(yǔ)言來(lái)對(duì)內(nèi)存進(jìn)行講解。
在后面的文章中所提到的內(nèi)存均指軟件層面上的內(nèi)存。
? 棧(Stack)
棧是一種常見(jiàn)的數(shù)據(jù)結(jié)構(gòu),棧只允許在結(jié)構(gòu)的一端操作數(shù)據(jù),所有數(shù)據(jù)都遵循后進(jìn)先出(Last-In First-Out,LIFO)的原則。
現(xiàn)實(shí)生活中最貼切的的例子就是羽毛球桶,通常我們只通過(guò)球桶的一側(cè)來(lái)進(jìn)行存取,最先放進(jìn)去的羽毛球只能最后被取出,而最后放進(jìn)去的則會(huì)最先被取出。
棧內(nèi)存之所以叫做棧內(nèi)存,是因?yàn)闂?nèi)存使用了棧的結(jié)構(gòu)。
棧內(nèi)存是一段連續(xù)的內(nèi)存空間,得益于棧結(jié)構(gòu)的簡(jiǎn)單直接,棧內(nèi)存的訪問(wèn)和操作速度都非???。
棧內(nèi)存的容量較小,主要用于存放函數(shù)調(diào)用信息和變量等數(shù)據(jù),大量的內(nèi)存分配操作會(huì)導(dǎo)致棧溢出(Stack overflow)。
棧內(nèi)存的數(shù)據(jù)儲(chǔ)存基本都是臨時(shí)性的,數(shù)據(jù)會(huì)在使用完之后立即被回收(如函數(shù)內(nèi)創(chuàng)建的局部變量在函數(shù)返回后就會(huì)被回收)。
簡(jiǎn)單來(lái)說(shuō):棧內(nèi)存適合存放生命周期短、占用空間小且固定的數(shù)據(jù)。
? 棧內(nèi)存的大小
棧內(nèi)存由操作系統(tǒng)直接管理,所以棧內(nèi)存的大小也由操作系統(tǒng)決定。
通常來(lái)說(shuō),每一條線程(Thread)都會(huì)有獨(dú)立的棧內(nèi)存空間,Windows 給每條線程分配的棧內(nèi)存默認(rèn)大小為 1MB。
? 堆(Heap)
堆也是一種常見(jiàn)的數(shù)據(jù)結(jié)構(gòu),但是不在本文討論范圍內(nèi),就不多說(shuō)了。
堆內(nèi)存雖然名字里有個(gè)“堆”字,但是它和數(shù)據(jù)結(jié)構(gòu)中的堆沒(méi)半毛錢關(guān)系,就只是撞了名罷了。
堆內(nèi)存是一大片內(nèi)存空間,堆內(nèi)存的分配是動(dòng)態(tài)且不連續(xù)的,程序可以按需申請(qǐng)堆內(nèi)存空間,但是訪問(wèn)速度要比棧內(nèi)存慢不少。
堆內(nèi)存里的數(shù)據(jù)可以長(zhǎng)時(shí)間存在,無(wú)用的數(shù)據(jù)需要程序主動(dòng)去回收,如果大量無(wú)用數(shù)據(jù)占用內(nèi)存就會(huì)造成內(nèi)存泄露(Memory leak)。
簡(jiǎn)單來(lái)說(shuō):堆內(nèi)存適合存放生命周期長(zhǎng),占用空間較大或占用空間不固定的數(shù)據(jù)。
? 堆內(nèi)存的上限
在 Node.js 中,堆內(nèi)存默認(rèn)上限在 64 位系統(tǒng)中約為 1.4 GB,在 32 位系統(tǒng)中約為 0.7 GB。
而在 Chrome 瀏覽器中,每個(gè)標(biāo)簽頁(yè)的內(nèi)存上限約為 4 GB(64 位系統(tǒng))和 1 GB(32 位系統(tǒng))。
? 進(jìn)程、線程與堆內(nèi)存
通常來(lái)說(shuō),一個(gè)進(jìn)程(Process)只會(huì)有一個(gè)堆內(nèi)存,同一進(jìn)程下的多個(gè)線程會(huì)共享同一個(gè)堆內(nèi)存。
在 Chrome 瀏覽器中,一般情況下每個(gè)標(biāo)簽頁(yè)都有單獨(dú)的進(jìn)程,不過(guò)在某些情況下也會(huì)出現(xiàn)多個(gè)標(biāo)簽頁(yè)共享一個(gè)進(jìn)程的情況。
明白了棧內(nèi)存與堆內(nèi)存是什么后,現(xiàn)在讓我們看看當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),棧內(nèi)存和堆內(nèi)存會(huì)發(fā)生什么變化。
當(dāng)函數(shù)被調(diào)用時(shí),會(huì)將函數(shù)推入棧內(nèi)存中,生成一個(gè)棧幀(Stack frame),棧幀可以理解為由函數(shù)的返回地址、參數(shù)和局部變量組成的一個(gè)塊;當(dāng)函數(shù)調(diào)用另一個(gè)函數(shù)時(shí),又會(huì)將另一個(gè)函數(shù)也推入棧內(nèi)存中,周而復(fù)始;直到最后一個(gè)函數(shù)返回,便從棧頂開(kāi)始將棧內(nèi)存中的元素逐個(gè)彈出,直到棧內(nèi)存中不再有元素時(shí)則此次調(diào)用結(jié)束。
上圖中的內(nèi)容經(jīng)過(guò)了簡(jiǎn)化,剝離了棧幀和各種指針的概念,主要展示函數(shù)調(diào)用以及內(nèi)存分配的大概過(guò)程。
在同一線程下(JavaScript 是單線程的),所有被執(zhí)行的函數(shù)以及函數(shù)的參數(shù)和局部變量都會(huì)被推入到同一個(gè)棧內(nèi)存中,這也就是大量遞歸會(huì)導(dǎo)致棧溢出(Stack overflow)的原因。
關(guān)于圖中涉及到的函數(shù)內(nèi)部變量?jī)?nèi)存分配的詳情請(qǐng)接著往下看。
當(dāng) JavaScript 程序運(yùn)行時(shí),在非全局作用域中產(chǎn)生的局部變量均儲(chǔ)存在棧內(nèi)存中。
但是,只有原始類型的變量是真正地把值儲(chǔ)存在棧內(nèi)存中。
而引用類型的變量只在棧內(nèi)存中儲(chǔ)存一個(gè)引用(reference),這個(gè)引用指向堆內(nèi)存里的真正的值。
? 原始類型(Primitive type)
原始類型又稱基本類型,包括
string
、number
、bigint
、boolean
、undefined
、null
和symbol
(ES6 新增)。原始類型的值被稱為原始值(Primitive value)。
補(bǔ)充:雖然
typeof null
返回的是'object'
,但是null
真的不是對(duì)象,會(huì)出現(xiàn)這樣的結(jié)果其實(shí)是 JavaScript 的一個(gè) Bug~
? 引用類型(Reference type)
除了原始類型外,其余類型都屬于引用類型,包括
Object
、Array
、Function
、Date
、RegExp
、String
、Number
、Boolean
等等…實(shí)際上
Object
是最基本的引用類型,其他引用類型均繼承自Object
。也就是說(shuō),所有引用類型的值實(shí)際上都是對(duì)象。引用類型的值被稱為引用值(Reference value)。
? 簡(jiǎn)單來(lái)說(shuō)
在多數(shù)情況下,原始類型的數(shù)據(jù)儲(chǔ)存在棧內(nèi)存,而引用類型的數(shù)據(jù)(對(duì)象)則儲(chǔ)存在堆內(nèi)存。
全局變量以及被閉包引用的變量(即使是原始類型)均儲(chǔ)存在堆內(nèi)存中。
? 全局變量(Global variables)
在全局作用域下創(chuàng)建的所有變量都會(huì)成為全局對(duì)象(如 window
對(duì)象)的屬性,也就是全局變量。
而全局對(duì)象儲(chǔ)存在堆內(nèi)存中,所以全局變量必然也會(huì)儲(chǔ)存在堆內(nèi)存中。
不要問(wèn)我為什么全局對(duì)象儲(chǔ)存在堆內(nèi)存中,一會(huì)我翻臉了??!
? 閉包(Closures)
在函數(shù)(局部作用域)內(nèi)創(chuàng)建的變量均為局部變量。
當(dāng)一個(gè)局部變量被當(dāng)前函數(shù)之外的其他函數(shù)所引用(也就是發(fā)生了逃逸),此時(shí)這個(gè)局部變量就不能隨著當(dāng)前函數(shù)的返回而被回收,那么這個(gè)變量就必須儲(chǔ)存在堆內(nèi)存中。
而這里的“其他函數(shù)”就是我們說(shuō)的閉包,就如下面這個(gè)例子:
function getCounter() { let count = 0; function counter() { return ++count; } return counter; } // closure 是一個(gè)閉包函數(shù) // 變量 count 發(fā)生了逃逸 let closure = getCounter(); closure(); // 1 closure(); // 2 closure(); // 3
閉包是一個(gè)非常重要且常用的概念,許多編程語(yǔ)言里都有閉包這個(gè)概念。這里就不詳細(xì)介紹了,貼一篇阮一峰大佬的文章。
學(xué)習(xí) JavaScript 閉包:http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
? 逃逸分析(Escape Analysis)
實(shí)際上,JavaScript 引擎會(huì)通過(guò)逃逸分析來(lái)決定變量是要儲(chǔ)存在棧內(nèi)存還是堆內(nèi)存中。
簡(jiǎn)單來(lái)說(shuō),逃逸分析是一種用來(lái)分析變量的作用域的機(jī)制。
棧內(nèi)存中會(huì)儲(chǔ)存兩種變量數(shù)據(jù):原始值和對(duì)象引用。
不僅類型不同,它們?cè)跅?nèi)存中的具體表現(xiàn)也不太一樣。
? Primitive values are immutable!
前面有說(shuō)到:原始類型的數(shù)據(jù)(原始值)直接儲(chǔ)存在棧內(nèi)存中。
⑴ 當(dāng)我們定義一個(gè)原始類型變量的時(shí)候,JavaScript 會(huì)在棧內(nèi)存中激活一塊內(nèi)存來(lái)儲(chǔ)存變量的值(原始值)。
⑵ 當(dāng)我們更改原始類型變量的值時(shí),實(shí)際上會(huì)再激活一塊新的內(nèi)存來(lái)儲(chǔ)存新的值,并將變量指向新的內(nèi)存空間,而不是改變?cè)瓉?lái)那塊內(nèi)存里的值。
⑶ 當(dāng)我們將一個(gè)原始類型變量賦值給另一個(gè)新的變量(也就是復(fù)制變量)時(shí),也是會(huì)再激活一塊新的內(nèi)存,并將源變量?jī)?nèi)存里的值復(fù)制一份到新的內(nèi)存里。
? 總之就是:棧內(nèi)存中的原始值一旦確定就不能被更改(不可變的)。
當(dāng)我們比較原始類型的變量時(shí),會(huì)直接比較棧內(nèi)存中的值,只要值相等那么它們就相等。
let a = '123'; let b = '123'; let c = '110'; let d = 123; console.log(a === b); // true console.log(a === c); // false console.log(a === d); // false
? Object references are mutable!
前面也有說(shuō)到:引用類型的變量在棧內(nèi)存中儲(chǔ)存的只是一個(gè)指向堆內(nèi)存的引用。
⑴ 當(dāng)我們定義一個(gè)引用類型的變量時(shí),JavaScript 會(huì)先在堆內(nèi)存中找到一塊合適的地方來(lái)儲(chǔ)存對(duì)象,并激活一塊棧內(nèi)存來(lái)儲(chǔ)存對(duì)象的引用(堆內(nèi)存地址),最后將變量指向這塊棧內(nèi)存。
? 所以當(dāng)我們通過(guò)變量訪問(wèn)對(duì)象時(shí),實(shí)際的訪問(wèn)過(guò)程應(yīng)該是:
變量 -> 棧內(nèi)存中的引用 -> 堆內(nèi)存中的值
⑵ 當(dāng)我們把引用類型變量賦值給另一個(gè)變量時(shí),會(huì)將源變量指向的棧內(nèi)存中的對(duì)象引用復(fù)制到新變量的棧內(nèi)存中,所以實(shí)際上只是復(fù)制了個(gè)對(duì)象引用,并沒(méi)有在堆內(nèi)存中生成一份新的對(duì)象。
⑶ 而當(dāng)我們給引用類型變量分配為一個(gè)新的對(duì)象時(shí),則會(huì)直接修改變量指向的棧內(nèi)存中的引用,新的引用指向堆內(nèi)存中新的對(duì)象。
? 總之就是:棧內(nèi)存中的對(duì)象引用是可以被更改的(可變的)。
所有引用類型的值實(shí)際上都是對(duì)象。
當(dāng)我們比較引用類型的變量時(shí),實(shí)際上是在比較棧內(nèi)存中的引用,只有引用相同時(shí)變量才相等。
即使是看起來(lái)完全一樣的兩個(gè)引用類型變量,只要他們的引用的不是同一個(gè)值,那么他們就是不一樣。
// 兩個(gè)變量指向的是兩個(gè)不同的引用 // 雖然這兩個(gè)對(duì)象看起來(lái)完全一樣 // 但它們確確實(shí)實(shí)是不同的對(duì)象實(shí)例 let a = { name: 'pp' } let b = { name: 'pp' } console.log(a === b); // false // 直接賦值的方式復(fù)制的是對(duì)象的引用 let c = a; console.log(a === c); // true
當(dāng)我們搞明白引用類型變量在內(nèi)存中的表現(xiàn)時(shí),就能清楚地理解為什么淺拷貝對(duì)象是不可靠的。
在淺拷貝中,簡(jiǎn)單的賦值只會(huì)復(fù)制對(duì)象的引用,實(shí)際上新變量和源變量引用的都是同一個(gè)對(duì)象,修改時(shí)也是修改的同一個(gè)對(duì)象,這顯然不是我們想要的。
想要真正的復(fù)制一個(gè)對(duì)象,就必須新建一個(gè)對(duì)象,將源對(duì)象的屬性復(fù)制過(guò)去;如果遇到引用類型的屬性,那就再新建一個(gè)對(duì)象,繼續(xù)復(fù)制…
此時(shí)我們就需要借助遞歸來(lái)實(shí)現(xiàn)多層次對(duì)象的復(fù)制,這也就是我們說(shuō)的深拷貝。
對(duì)于任何引用類型的變量,都應(yīng)該使用深拷貝來(lái)復(fù)制,除非你很確定你的目的就是復(fù)制一個(gè)引用。
通常來(lái)說(shuō),所有應(yīng)用程序的內(nèi)存生命周期都是基本一致的:
分配 -> 使用 -> 釋放
當(dāng)我們使用高級(jí)語(yǔ)言編寫程序時(shí),往往不會(huì)涉及到內(nèi)存的分配與釋放操作,因?yàn)榉峙渑c釋放均已經(jīng)在底層語(yǔ)言中實(shí)現(xiàn)了。
對(duì)于 JavaScript 程序來(lái)說(shuō),內(nèi)存的分配與釋放是由 JavaScript 引擎自動(dòng)完成的(目前的 JavaScript 引擎基本都是使用 C++ 或 C 編寫的)。
但是這不意味著我們就不需要在乎內(nèi)存管理,了解內(nèi)存的更多細(xì)節(jié)可以幫助我們寫出性能更好,穩(wěn)定性更高的代碼。
垃圾回收即我們常說(shuō)的 GC(Garbage collection),也就是清除內(nèi)存中不再需要的數(shù)據(jù),釋放內(nèi)存空間。
由于棧內(nèi)存由操作系統(tǒng)直接管理,所以當(dāng)我們提到 GC 時(shí)指的都是堆內(nèi)存的垃圾回收。
基本上現(xiàn)在的瀏覽器的 JavaScript 引擎(如 V8 和 SpiderMonkey)都實(shí)現(xiàn)了垃圾回收機(jī)制,引擎中的垃圾回收器(Garbage collector)會(huì)定期進(jìn)行垃圾回收。
? 緊急補(bǔ)課
在我們繼續(xù)之前,必須先了解“可達(dá)性”和“內(nèi)存泄露”這兩個(gè)概念:
? 可達(dá)性(Reachability)
在 JavaScript 中,可達(dá)性指的是一個(gè)變量是否能夠直接或間接通過(guò)全局對(duì)象訪問(wèn)到,如果可以那么該變量就是可達(dá)的(Reachable),否則就是不可達(dá)的(Unreachable)。
上圖中的節(jié)點(diǎn) 9 和節(jié)點(diǎn) 10 均無(wú)法通過(guò)節(jié)點(diǎn) 1(根節(jié)點(diǎn))直接或間接訪問(wèn),所以它們都是不可達(dá)的,可以被安全地回收。
? 內(nèi)存泄漏(Memory leak)
內(nèi)存泄露指的是程序運(yùn)行時(shí)由于某種原因未能釋放那些不再使用的內(nèi)存,造成內(nèi)存空間的浪費(fèi)。
輕微的內(nèi)存泄漏或許不太會(huì)對(duì)程序造成什么影響,但是一旦泄露變嚴(yán)重,就會(huì)開(kāi)始影響程序的性能,甚至導(dǎo)致程序的崩潰。
垃圾回收的基本思路很簡(jiǎn)單:確定哪個(gè)變量不會(huì)再使用,然后釋放它占用的內(nèi)存。
實(shí)際上,在回收過(guò)程中想要確定一個(gè)變量是否還有用并不簡(jiǎn)單。
直到現(xiàn)在也還沒(méi)有一個(gè)真正完美的垃圾回收算法,接下來(lái)介紹 3 種最廣為人知的垃圾回收算法。
標(biāo)記清除算法是目前最常用的垃圾收集算法之一。
從該算法的名字上就可以看出,算法的關(guān)鍵就是標(biāo)記與清除。
標(biāo)記指的是標(biāo)記變量的狀態(tài)的過(guò)程,標(biāo)記變量的具體方法有很多種,但是基本理念是相似的。
對(duì)于標(biāo)記算法我們不需要知道所有細(xì)節(jié),只需明白標(biāo)記的基本原理即可。
需要注意的是,這個(gè)算法的效率不算高,同時(shí)會(huì)引起內(nèi)存碎片化的問(wèn)題。
? 舉個(gè)栗子
當(dāng)一個(gè)變量進(jìn)入執(zhí)行上下文時(shí),它就會(huì)被標(biāo)記為“處于上下文中”;而當(dāng)變量離開(kāi)執(zhí)行上下文時(shí),則會(huì)被標(biāo)記為“已離開(kāi)上下文”。
? 執(zhí)行上下文(Execution context)
執(zhí)行上下文是 JavaScript 中非常重要的概念,簡(jiǎn)單來(lái)說(shuō)的是代碼執(zhí)行的環(huán)境。
如果你現(xiàn)在對(duì)于執(zhí)行上下文還不是很了解,我強(qiáng)烈建議你抽空專門去學(xué)習(xí)下!??!
垃圾回收器將定期掃描內(nèi)存中的所有變量,將處于上下文中以及被處于上下文中的變量引用的變量的標(biāo)記去除,將其余變量標(biāo)記為“待刪除”。
隨后,垃圾回收器會(huì)清除所有帶有“待刪除”標(biāo)記的變量,并釋放它們所占用的內(nèi)存。
準(zhǔn)確來(lái)說(shuō),Compact 應(yīng)譯為緊湊、壓縮,但是在這里我覺(jué)得用“整理”更為貼切。
標(biāo)記整理算法也是常用的垃圾收集算法之一。
使用標(biāo)記整理算法可以解決內(nèi)存碎片化的問(wèn)題(通過(guò)整理),提高內(nèi)存空間的可用性。
但是,該算法的標(biāo)記階段比較耗時(shí),可能會(huì)堵塞主線程,導(dǎo)致程序長(zhǎng)時(shí)間處于無(wú)響應(yīng)狀態(tài)。
雖然算法的名字上只有標(biāo)記和整理,但這個(gè)算法通常有 3 個(gè)階段,即標(biāo)記、整理與清除。
? 以 V8 的標(biāo)記整理算法為例
① 首先,在標(biāo)記階段,垃圾回收器會(huì)從全局對(duì)象(根)開(kāi)始,一層一層往下查詢,直到標(biāo)記完所有活躍的對(duì)象,那么剩下的未被標(biāo)記的對(duì)象就是不可達(dá)的了。
② 然后是整理階段(碎片整理),垃圾回收器會(huì)將活躍的(被標(biāo)記了的)對(duì)象往內(nèi)存空間的一端移動(dòng),這個(gè)過(guò)程可能會(huì)改變內(nèi)存中的對(duì)象的內(nèi)存地址。
③ 最后來(lái)到清除階段,垃圾回收器會(huì)將邊界后面(也就是最后一個(gè)活躍的對(duì)象后面)的對(duì)象清除,并釋放它們占用的內(nèi)存空間。
引用計(jì)數(shù)算法是基于“引用計(jì)數(shù)”實(shí)現(xiàn)的垃圾回收算法,這是最初級(jí)但已經(jīng)被棄用的垃圾回收算法。
引用計(jì)數(shù)算法需要 JavaScript 引擎在程序運(yùn)行時(shí)記錄每個(gè)變量被引用的次數(shù),隨后根據(jù)引用的次數(shù)來(lái)判斷變量是否能夠被回收。
雖然垃圾回收已不再使用引用計(jì)數(shù)算法,但是引用計(jì)數(shù)技術(shù)仍非常有用!
? 舉個(gè)栗子
注意:垃圾回收不是即使生效的!但是在下面的例子中我們將假設(shè)回收是立即生效的,這樣會(huì)更好理解~
// 下面我將 name 屬性為 ππ 的對(duì)象簡(jiǎn)稱為 ππ // 而 name 屬性為 pp 的對(duì)象則簡(jiǎn)稱為 pp // ππ 的引用:1,pp 的引用:1 let a = { name: 'ππ', z: { name: 'pp' } } // b 和 a 都指向 ππ // ππ 的引用:2,pp 的引用:1 let b = a; // x 和 a.z 都指向 pp // ππ 的引用:2,pp 的引用:2 let x = a.z; // 現(xiàn)在只有 b 還指向 ππ // ππ 的引用:1,pp 的引用:2 a = null; // 現(xiàn)在 ππ 沒(méi)有任何引用了,可以被回收了 // 在 ππ 被回收后,pp 的引用也會(huì)相應(yīng)減少 // ππ 的引用:0,pp 的引用:1 b = null; // 現(xiàn)在 pp 也可以被回收了 // ππ 的引用:0,pp 的引用:0 x = null; // 哦豁,這下全完了!
? 循環(huán)引用(Circular references)
引用計(jì)數(shù)算法看似很美好,但是它有一個(gè)致命的缺點(diǎn),就是無(wú)法處理循環(huán)引用的情況。
在下方的例子中,當(dāng) foo()
函數(shù)執(zhí)行完畢之后,對(duì)象 a
與 b
都已經(jīng)離開(kāi)了作用域,理論上它們都應(yīng)該能夠被回收才對(duì)。
但是由于它們互相引用了對(duì)方,所以垃圾回收器就認(rèn)為他們都還在被引用著,導(dǎo)致它們哥倆永遠(yuǎn)都不會(huì)被回收,這就造成了內(nèi)存泄露。
function foo() { let a = { o: null }; let b = { o: null }; a.o = b; b.o = a; } foo(); // 即使 foo 函數(shù)已經(jīng)執(zhí)行完畢 // 對(duì)象 a 和 b 均已離開(kāi)函數(shù)作用域 // 但是 a 和 b 還在互相引用 // 那么它們這輩子都不會(huì)被回收了 // Oops!內(nèi)存泄露了!
8?? V8
V8 是一個(gè)由 Google 開(kāi)源的用 C++ 編寫的高性能 JavaScript 引擎。
V8 是目前最流行的 JavaScript 引擎之一,我們熟知的 Chrome 瀏覽器和 Node.js 等軟件都在使用 V8。
在 V8 的內(nèi)存管理機(jī)制中,把堆內(nèi)存(Heap memory)劃分成了多個(gè)區(qū)域。
這里我們只關(guān)注這兩個(gè)區(qū)域:
New Space(新空間):又稱 Young generation(新世代),用于儲(chǔ)存新生成的對(duì)象,由 Minor GC 進(jìn)行管理。
Old Space(舊空間):又稱 Old generation(舊世代),用于儲(chǔ)存那些在兩次 GC 后仍然存活的對(duì)象,由 Major GC 進(jìn)行管理。
也就是說(shuō),只要 New Space 里的對(duì)象熬過(guò)了兩次 GC,就會(huì)被轉(zhuǎn)移到 Old Space,變成老油條。
? 雙管齊下
V8 內(nèi)部實(shí)現(xiàn)了兩個(gè)垃圾回收器:
Minor GC(副 GC):它還有個(gè)名字叫做 Scavenger(清道夫),具體使用的是 Cheney’s Algorithm(Cheney 算法)。
Major GC(主 GC):使用的是文章前面提到的 Mark-Compact Algorithm(標(biāo)記-整理算法)。
儲(chǔ)存在 New Space 里的新生對(duì)象大多都只是臨時(shí)使用的,而且 New Space 的容量比較小,為了保持內(nèi)存的可用率,Minor GC 會(huì)頻繁地運(yùn)行。
而 Old Space 里的對(duì)象存活時(shí)間都比較長(zhǎng),所以 Major GC 沒(méi)那么勤快,這一定程度地降低了頻繁 GC 帶來(lái)的性能損耗。
? 加點(diǎn)魔法
我們?cè)谏戏降摹皹?biāo)記整理算法”中有提到這個(gè)算法的標(biāo)記過(guò)程非常耗時(shí),所以很容易導(dǎo)致應(yīng)用長(zhǎng)時(shí)間無(wú)響應(yīng)。
為了提升用戶體驗(yàn),V8 還實(shí)現(xiàn)了一個(gè)名為增量標(biāo)記(Incremental marking)的特性。
增量標(biāo)記的要點(diǎn)就是把標(biāo)記工作分成多個(gè)小段,夾雜在主線程(Main thread)的 JavaScript 邏輯中,這樣就不會(huì)長(zhǎng)時(shí)間阻塞主線程了。
當(dāng)然增量標(biāo)記也有代價(jià)的,在增量標(biāo)記過(guò)程中所有對(duì)象的變化都需要通知垃圾回收器,好讓垃圾回收器能夠正確地標(biāo)記那些對(duì)象,這里的“通知”也是需要成本的。
另外 V8 中還有使用工作線程(Worker thread)實(shí)現(xiàn)的平行標(biāo)記(Parallel marking)和并行標(biāo)記(Concurrent marking),這里我就不再細(xì)說(shuō)了~
? 總結(jié)一下
為了提升性能和用戶體驗(yàn),V8 內(nèi)部做了非常非常多的“騷操作”,本文提到的都只是冰山一角,但足以讓我五體投地佩服連連!
總之就是非常 Amazing 啊~
或者說(shuō)是:內(nèi)存優(yōu)化(Memory optimization)?
雖然我們寫代碼的時(shí)候一般不會(huì)直接接觸內(nèi)存管理,但是有一些注意事項(xiàng)可以讓我們避免引起內(nèi)存問(wèn)題,甚至提升代碼的性能。
全局變量的訪問(wèn)速度遠(yuǎn)不及局部變量,應(yīng)盡量避免定義非必要的全局變量。
在我們實(shí)際的項(xiàng)目開(kāi)發(fā)中,難免會(huì)需要去定義一些全局變量,但是我們必須謹(jǐn)慎使用全局變量。
因?yàn)槿肿兞坑肋h(yuǎn)都是可達(dá)的,所以全局變量永遠(yuǎn)不會(huì)被回收。
? 還記得“可達(dá)性”這個(gè)概念嗎?
因?yàn)槿肿兞恐苯訏燧d在全局對(duì)象上,也就是說(shuō)全局變量永遠(yuǎn)都可以通過(guò)全局對(duì)象直接訪問(wèn)。
所以全局變量永遠(yuǎn)都是可達(dá)的,而可達(dá)的變量永遠(yuǎn)都不會(huì)被回收。
? 應(yīng)該怎么做?
當(dāng)一個(gè)全局變量不再需要用到時(shí),記得解除其引用(置空),好讓垃圾回收器可以釋放這部分內(nèi)存。
// 全局變量不會(huì)被回收 window.me = { name: '吳彥祖', speak: function() { console.log(`我是${this.name}`); } }; window.me.speak(); // 解除引用后才可以被回收 window.me = null;
實(shí)際上的隱藏類遠(yuǎn)比本文所提到的復(fù)雜,但是今天的主角不是它,所以我們點(diǎn)到為止。
在 V8 內(nèi)部有一個(gè)叫做“隱藏類”的機(jī)制,主要用于提升對(duì)象(Object)的性能。
V8 里的每一個(gè) JS 對(duì)象(JS Objects)都會(huì)關(guān)聯(lián)一個(gè)隱藏類,隱藏類里面儲(chǔ)存了對(duì)象的形狀(特征)和屬性名稱到屬性的映射等信息。
隱藏類內(nèi)記錄了每個(gè)屬性的內(nèi)存偏移(Memory offset),后續(xù)訪問(wèn)屬性的時(shí)候就可以快速定位到對(duì)應(yīng)屬性的內(nèi)存位置,從而提升對(duì)象屬性的訪問(wèn)速度。
在我們創(chuàng)建對(duì)象時(shí),擁有完全相同的特征(相同屬性且相同順序)的對(duì)象可以共享同一個(gè)隱藏類。
? 再想象一下
我們可以把隱藏類想象成工業(yè)生產(chǎn)中使用的模具,有了模具之后,產(chǎn)品的生產(chǎn)效率得到了很大的提升。
但是如果我們更改了產(chǎn)品的形狀,那么原來(lái)的模具就不能用了,又需要制作新的模具才行。
? 舉個(gè)栗子
在 Chrome 瀏覽器 Devtools 的 Console 面板中執(zhí)行以下代碼:
// 對(duì)象 A let objectA = { id: 'A', name: '吳彥祖' }; // 對(duì)象 B let objectB = { id: 'B', name: '彭于晏' }; // 對(duì)象 C let objectC = { id: 'C', name: '劉德華', gender: '男' }; // 對(duì)象 A 和 B 擁有完全相同的特征 // 所以它們可以使用同一個(gè)隱藏類 // good!
隨后在 Memory 面板打一個(gè)堆快照,通過(guò)堆快照中的 Comparison 視圖可以快速找到上面創(chuàng)建的 3 個(gè)對(duì)象:
注:關(guān)于如何查看內(nèi)存中的對(duì)象將會(huì)在文章的第二大部分中進(jìn)行講解,現(xiàn)在讓我們專注于隱藏類。
在上圖中可以很清楚地看到對(duì)象 A 和 B 確實(shí)使用了同一個(gè)隱藏類。
而對(duì)象 C 因?yàn)槎嗔艘粋€(gè) gender
屬性,所以不能和前面兩個(gè)對(duì)象共享隱藏類。
? 動(dòng)態(tài)增刪對(duì)象屬性
一般情況下,當(dāng)我們動(dòng)態(tài)修改對(duì)象的特征(增刪屬性)時(shí),V8 會(huì)為該對(duì)象分配一個(gè)能用的隱藏類或者創(chuàng)建一個(gè)新的隱藏類(新的分支)。
例如動(dòng)態(tài)地給對(duì)象增加一個(gè)新的屬性:
注:這種操作被稱為“先創(chuàng)建再補(bǔ)充(ready-fire-aim)”。
// 增加 gender 屬性 objectB.gender = '男'; // 對(duì)象 B 的特征發(fā)生了變化 // 多了一個(gè)原本沒(méi)有的 gender 屬性 // 導(dǎo)致對(duì)象 B 不能再與 A 共享隱藏類 // bad!
動(dòng)態(tài)刪除(delete
)對(duì)象的屬性也會(huì)導(dǎo)致同樣的結(jié)果:
// 刪除 name 屬性 delete objectB.name; // A:我們不一樣! // bad!
不過(guò),添加數(shù)組索引屬性(Array-indexed properties)并不會(huì)有影響:
其實(shí)就是用整數(shù)作為屬性名,此時(shí) V8 會(huì)另外處理。
// 增加 1 屬性 objectB[1] = '數(shù)字組引屬性'; // 不影響共享隱藏類 // so far so good!
? 那問(wèn)題來(lái)了
說(shuō)了這么多,隱藏類看起來(lái)確實(shí)可以提升性能,那它和內(nèi)存又有什么關(guān)系呢?
實(shí)際上,隱藏類也需要占用內(nèi)存空間,這其實(shí)就是一種用空間換時(shí)間的機(jī)制。
如果由于動(dòng)態(tài)增刪對(duì)象屬性而創(chuàng)建了大量隱藏類和分支,結(jié)果就是會(huì)浪費(fèi)不少內(nèi)存空間。
? 舉個(gè)栗子
創(chuàng)建 1000 個(gè)擁有相同屬性的對(duì)象,內(nèi)存中只會(huì)多出 1 個(gè)隱藏類。
而創(chuàng)建 1000 個(gè)屬性信息完全不同的對(duì)象,內(nèi)存中就會(huì)多出 1000 個(gè)隱藏類。
? 應(yīng)該怎么做?
所以,我們要盡量避免動(dòng)態(tài)增刪對(duì)象屬性操作,應(yīng)該在構(gòu)造函數(shù)內(nèi)就一次性聲明所有需要用到的屬性。
如果確實(shí)不再需要某個(gè)屬性,我們可以將屬性的值設(shè)為 null
,如下:
// 將 age 屬性置空 objectB.age = null; // still good!
另外,相同名稱的屬性盡量按照相同的順序來(lái)聲明,可以盡可能地讓更多對(duì)象共享相同的隱藏類。
即使遇到不能共享隱藏類的情況,也至少可以減少隱藏類分支的產(chǎn)生。
其實(shí)動(dòng)態(tài)增刪對(duì)象屬性所引起的性能問(wèn)題更為關(guān)鍵,但因本文篇幅有限,就不再展開(kāi)了。
前面有提到:被閉包引用的變量?jī)?chǔ)存在堆內(nèi)存中。
這里我們?cè)僦攸c(diǎn)關(guān)注一下閉包中的內(nèi)存問(wèn)題,還是前面的例子:
function getCounter() { let count = 0; function counter() { return ++count; } return counter; } // closure 是一個(gè)閉包函數(shù) let closure = getCounter(); closure(); // 1 closure(); // 2 closure(); // 3
現(xiàn)在只要我們一直持有變量(函數(shù)) closure
,那么變量 count
就不會(huì)被釋放。
或許你還沒(méi)有發(fā)現(xiàn)風(fēng)險(xiǎn)所在,不如讓我們?cè)囅胱兞?count
不是一個(gè)數(shù)字,而是一個(gè)巨大的數(shù)組,一但這樣的閉包多了,那對(duì)于內(nèi)存來(lái)說(shuō)就是災(zāi)難。
// 我將這個(gè)作品稱為:閉包炸彈 function closureBomb() { const handsomeBoys = []; setInterval(() => { for (let i = 0; i < 100; i++) { handsomeBoys.push( { name: '陳皮皮', rank: 0 }, { name: ' 你 ', rank: 1 }, { name: '吳彥祖', rank: 2 }, { name: '彭于晏', rank: 3 }, { name: '劉德華', rank: 4 }, { name: '郭富城', rank: 5 } ); } }, 100); } closureBomb(); // 即將毀滅世界 // ? ? ? ?
? 應(yīng)該怎么做?
所以,我們必須避免濫用閉包,并且謹(jǐn)慎使用閉包!
當(dāng)不再需要時(shí)記得解除閉包函數(shù)的引用,讓閉包函數(shù)以及引用的變量能夠被回收。
closure = null; // 變量 count 終于得救了
說(shuō)了這么多,那我們應(yīng)該如何查看并分析程序運(yùn)行時(shí)的內(nèi)存情況呢?
“工欲善其事,必先利其器?!?/p>
對(duì)于 Web 前端項(xiàng)目來(lái)說(shuō),分析內(nèi)存的最佳工具非 Memory 莫屬!
這里的 Memory 指的是 DevTools 中的一個(gè)工具,為了避免混淆,下面我會(huì)用“Memory 面板”或”內(nèi)存面板“代稱。
? DevTools(開(kāi)發(fā)者工具)
DevTools 是瀏覽器里內(nèi)置的一套用于 Web 開(kāi)發(fā)和調(diào)試的工具。
使用 Chromuim 內(nèi)核的瀏覽器都帶有 DevTools,個(gè)人推薦使用 Chrome 或者 Edge(新)。
在我們切換到 Memory 面板后,會(huì)看到以下界面(注意標(biāo)注):
在這個(gè)面板中,我們可以通過(guò) 3 種方式來(lái)記錄內(nèi)存情況:
Heap snapshot:堆快照
Allocation instrumentation on timeline:內(nèi)存分配時(shí)間軸
Allocation sampling:內(nèi)存分配采樣
小貼士:點(diǎn)擊面板左上角的 Collect garbage 按鈕(垃圾桶圖標(biāo))可以主動(dòng)觸發(fā)垃圾回收。
? 在正式開(kāi)始分析內(nèi)存之前,讓我們先學(xué)習(xí)幾個(gè)重要的概念:
? Shallow Size(淺層大?。?/strong>
淺層大小指的是當(dāng)前對(duì)象自身占用的內(nèi)存大小。
淺層大小不包含自身引用的對(duì)象。
? Retained Size(保留大?。?/strong>
保留大小指的是當(dāng)前對(duì)象被 GC 回收后總共能夠釋放的內(nèi)存大小。
換句話說(shuō),也就是當(dāng)前對(duì)象自身大小加上對(duì)象直接或間接引用的其他對(duì)象的大小總和。
需要注意的是,保留大小不包含那些除了被當(dāng)前對(duì)象引用之外還被全局對(duì)象直接或間接引用的對(duì)象。
堆快照可以記錄頁(yè)面當(dāng)前時(shí)刻的 JS 對(duì)象以及 DOM 節(jié)點(diǎn)的內(nèi)存分配情況。
? 如何開(kāi)始
點(diǎn)擊頁(yè)面底部的 Take snapshot 按鈕或者左上角的 ? 按鈕即可打一個(gè)堆快照,片刻之后就會(huì)自動(dòng)展示結(jié)果。
在堆快照結(jié)果頁(yè)面中,我們可以使用 4 種不同的視圖來(lái)觀察內(nèi)存情況:
Summary:摘要視圖
Comparison:比較視圖
Containment:包含視圖
Statistics:統(tǒng)計(jì)視圖
默認(rèn)顯示 Summary 視圖。
摘要視圖根據(jù) Constructor(構(gòu)造函數(shù))來(lái)將對(duì)象進(jìn)行分組,我們可以在 Class filter(類過(guò)濾器)中輸入構(gòu)造函數(shù)名稱來(lái)快速篩選對(duì)象。
頁(yè)面中的幾個(gè)關(guān)鍵詞:
Constructor:構(gòu)造函數(shù)。
Distance:(根)距離,對(duì)象與 GC 根之間的最短距離。
Shallow Size:淺層大小,單位:Bytes(字節(jié))。
Retained Size:保留大小,單位:Bytes(字節(jié))。
Retainers:持有者,也就是直接引用目標(biāo)對(duì)象的變量。
? Retainers(持有者)
Retainers 欄在舊版的 Devtools 里叫做 Object’s retaining tree(對(duì)象保留樹(shù))。
Retainers 下的對(duì)象也展開(kāi)為樹(shù)形結(jié)構(gòu),方便我們進(jìn)行引用溯源。
在視圖中的構(gòu)造函數(shù)列表中,有一些用“()”包裹的條目:
(compiled code):已編譯的代碼。
(closure):閉包函數(shù)。
(array, string, number, symbol, regexp):對(duì)應(yīng)類型(Array
、String
、Number
、Symbol
、RegExp
)的數(shù)據(jù)。
(concatenated string):使用 concat()
函數(shù)拼接而成的字符串。
(sliced string):使用 slice()
、substring()
等函數(shù)進(jìn)行邊緣切割的字符串。
(system):系統(tǒng)(引擎)產(chǎn)生的對(duì)象,如 V8 創(chuàng)建的 HiddenClasses(隱藏類)和 DescriptorArrays(描述符數(shù)組)等數(shù)據(jù)。
? DescriptorArrays(描述符數(shù)組)
描述符數(shù)組主要包含對(duì)象的屬性名信息,是隱藏類的重要組成部分。
不過(guò)描述符數(shù)組內(nèi)不會(huì)包含整數(shù)索引屬性。
而其余沒(méi)有用“()”包裹的則為全局屬性和 GC 根。
另外,每個(gè)對(duì)象后面都會(huì)有一串“@”開(kāi)頭的數(shù)字,這是對(duì)象在內(nèi)存中的唯一 ID。
小貼士:按下快捷鍵 Ctrl/Command + F 展示搜索欄,輸入名稱或 ID 即可快速查找目標(biāo)對(duì)象。
? 實(shí)踐一下:實(shí)例化一個(gè)對(duì)象
① 切換到 Console 面板,執(zhí)行以下代碼來(lái)實(shí)例化一個(gè)對(duì)象:
function TestClass() { this.number = 123; this.string = 'abc'; this.boolean = true; this.symbol = Symbol('test'); this.undefined = undefined; this.null = null; this.object = { name: 'pp' }; this.array = [1, 2, 3]; this.getSet = { _value: 0, get value() { return this._value; }, set value(v) { this._value = v; } }; } let testObject = new TestClass();
② 回到 Memory 面板,打一個(gè)堆快照,在 Class filter 中輸入“TestClass”:
可以看到內(nèi)存中有一個(gè) TestClass
的實(shí)例,該實(shí)例的淺層大小為 80 字節(jié),保留大小為 876 字節(jié)。
? 注意到了嗎?
堆快照中的
TestClass
實(shí)例的屬性中少了一個(gè)名為number
屬性,這是因?yàn)槎芽煺詹粫?huì)捕捉數(shù)字屬性。
? 實(shí)踐一下:創(chuàng)建一個(gè)字符串
① 切換到 Console 面板,執(zhí)行以下代碼來(lái)創(chuàng)建一個(gè)字符串:
// 這是一個(gè)全局變量 let testString = '我是吳彥祖';
② 回到 Memory 面板,打一個(gè)堆快照,打開(kāi)搜索欄(Ctrl/Command + F)并輸入“我是吳彥祖”:
只有同時(shí)存在 2 個(gè)或以上的堆快照時(shí)才會(huì)出現(xiàn) Comparison 選項(xiàng)。
比較視圖用于展示兩個(gè)堆快照之間的差異。
使用比較視圖可以讓我們快速得知在執(zhí)行某個(gè)操作后的內(nèi)存變化情況(如新增或減少對(duì)象)。
通過(guò)多個(gè)快照的對(duì)比還可以讓我們快速判斷并定位內(nèi)存泄漏。
文章前面提到隱藏類的時(shí)候,就是使用了比較視圖來(lái)快速查找新創(chuàng)建的對(duì)象。
? 實(shí)踐一下
① 新建一個(gè)無(wú)痕(匿名)標(biāo)簽頁(yè)并切換到 Memory 面板,打一個(gè)堆快照 Snapshot 1。
? 為什么是無(wú)痕標(biāo)簽頁(yè)?
普通標(biāo)簽頁(yè)會(huì)受到瀏覽器擴(kuò)展或者其他腳本影響,內(nèi)存占用不穩(wěn)定。
使用無(wú)痕窗口的標(biāo)簽頁(yè)可以保證頁(yè)面的內(nèi)存相對(duì)純凈且穩(wěn)定,有利于我們進(jìn)行對(duì)比。
另外,建議打開(kāi)窗口一段之間之后再開(kāi)始測(cè)試,這樣內(nèi)存會(huì)比較穩(wěn)定(控制變量)。
② 切換到 Console 面板,執(zhí)行以下代碼來(lái)實(shí)例化一個(gè) Foo
對(duì)象:
function Foo() { this.name = 'pp'; this.age = 18; } let foo = new Foo();
③ 回到 Memory 面板,再打一個(gè)堆快照 Snapshot 2,切換到 Comparison 視圖,選擇 Snapshot 1 作為 Base snapshot(基本快照),在 Class filter 中輸入“Foo”:
可以看到內(nèi)存中新增了一個(gè) Foo
對(duì)象實(shí)例,分配了 52 字節(jié)內(nèi)存空間,該實(shí)例的引用持有者為變量 foo
。
④ 再次切換到 Console 面板,執(zhí)行以下代碼來(lái)解除變量 foo
的引用:
// 解除對(duì)象的引用 foo = null;
⑤ 再回到 Memory 面板,打一個(gè)堆快照 Snapshot 3,選擇 Snapshot 2 作為 Base snapshot,在 Class filter 中輸入“Foo”:
內(nèi)存中的 Foo
對(duì)象實(shí)例已經(jīng)被刪除,釋放了 52 字節(jié)的內(nèi)存空間。
包含視圖就是程序?qū)ο蠼Y(jié)構(gòu)的“鳥(niǎo)瞰圖(Bird’s eye view)”,允許我們通過(guò)全局對(duì)象出發(fā),一層一層往下探索,從而了解內(nèi)存的詳細(xì)情況。
包含視圖中有以下幾種全局對(duì)象:
GC roots(GC 根)
GC roots 就是 JavaScript 虛擬機(jī)的垃圾回收中實(shí)際使用的根節(jié)點(diǎn)。
GC 根可以由 Built-in object maps(內(nèi)置對(duì)象映射)、Symbol tables(符號(hào)表)、VM thread stacks(VM 線程堆棧)、Compilation caches(編譯緩存)、Handle scopes(句柄作用域)和 Global handles(全局句柄)等組成。
DOMWindow objects(DOMWindow 對(duì)象)
DOMWindow objects 指的是由宿主環(huán)境(瀏覽器)提供的頂級(jí)對(duì)象,也就是 JavaScript 代碼中的全局對(duì)象 window
,每個(gè)標(biāo)簽頁(yè)都有自己的 window
對(duì)象(即使是同一窗口)。
Native objects(原生對(duì)象)
Native objects 指的是那些基于 ECMAScript 標(biāo)準(zhǔn)實(shí)現(xiàn)的內(nèi)置對(duì)象,包括 Object
、Function
、Array
、String
、Boolean
、Number
、Date
、RegExp
、Math
等對(duì)象。
? 實(shí)踐一下
① 切換到 Console 面板,執(zhí)行以下代碼來(lái)創(chuàng)建一個(gè)構(gòu)造函數(shù) $ABC
:
構(gòu)造函數(shù)命名前面加個(gè) $ 是因?yàn)檫@樣排序的時(shí)候可以排在前面,方便找。
function $ABC() { this.name = 'pp'; }
② 切換到 Memory 面板,打一個(gè)堆快照,切換為 Containment 視圖:
在當(dāng)前標(biāo)簽頁(yè)的全局對(duì)象下就可以找到我們剛剛創(chuàng)建的構(gòu)造函數(shù) $ABC
。
統(tǒng)計(jì)視圖可以很直觀地展示內(nèi)存整體分配情況。
在該視圖里的空心餅圖中共有 6 種顏色,各含義分別為:
紅色:Code(代碼)
綠色:Strings(字符串)
藍(lán)色:JS arrays(數(shù)組)
橙色:Typed arrays(類型化數(shù)組)
紫色:System objects(系統(tǒng)對(duì)象)
白色:空閑內(nèi)存
在一段時(shí)間內(nèi)持續(xù)地記錄內(nèi)存分配(約每 50 毫秒打一張堆快照),記錄完成后可以選擇查看任意時(shí)間段的內(nèi)存分配詳情。
另外還可以勾選同時(shí)記錄分配堆棧(Allocation stacks),也就是記錄調(diào)用堆棧,不過(guò)這會(huì)產(chǎn)生額外的性能消耗。
? 如何開(kāi)始
點(diǎn)擊頁(yè)面底部的 Start 按鈕或者左上角的 ? 按鈕即可開(kāi)始記錄,記錄過(guò)程中點(diǎn)擊左上角的 ? 按鈕來(lái)結(jié)束記錄,片刻之后就會(huì)自動(dòng)展示結(jié)果。
? 操作一下
① 打開(kāi) Memory 面板,開(kāi)始記錄分配時(shí)間軸。
② 切換到 Console 面板,執(zhí)行以下代碼:
代碼效果:每隔 1 秒鐘創(chuàng)建 100 個(gè)對(duì)象,共創(chuàng)建 1000 個(gè)對(duì)象。
console.log('測(cè)試開(kāi)始'); let objects = []; let handler = setInterval(() => { // 每秒創(chuàng)建 100 個(gè)對(duì)象 for (let i = 0; i < 100; i++) { const name = `n${objects.length}`; const value = `v${objects.length}`; objects.push({ [name]: value}); } console.log(`對(duì)象數(shù)量:${objects.length}`); // 達(dá)到 1000 個(gè)后停止 if (objects.length >= 1000) { clearInterval(handler); console.log('測(cè)試結(jié)束'); } }, 1000);
? 又是一個(gè)細(xì)節(jié)
不知道你有沒(méi)有發(fā)現(xiàn),在上面的代碼中,我干了一件壞事。
在 for 循環(huán)創(chuàng)建對(duì)象時(shí),會(huì)根據(jù)對(duì)象數(shù)組當(dāng)前長(zhǎng)度生成一個(gè)唯一的屬性名和屬性值。
這樣一來(lái) V8 就無(wú)法對(duì)這些對(duì)象進(jìn)行優(yōu)化,方便我們進(jìn)行測(cè)試。
另外,如果直接使用對(duì)象數(shù)組的長(zhǎng)度作為屬性名會(huì)有驚喜~
③ 靜靜等待 10 秒鐘,控制臺(tái)會(huì)打印出“測(cè)試結(jié)束”。
④ 切換回 Memory 面板,停止記錄,片刻之后會(huì)自動(dòng)進(jìn)入結(jié)果頁(yè)面。
分配時(shí)間軸結(jié)果頁(yè)有 4 種視圖:
Summary:摘要視圖
Containment:包含視圖
Allocation:分配視圖
Statistics:統(tǒng)計(jì)視圖
默認(rèn)顯示 Summary 視圖。
看起來(lái)和堆快照的摘要視圖很相似,主要是頁(yè)面上方多了一條橫向的時(shí)間軸(Timeline)。
? 時(shí)間軸
時(shí)間軸中主要的 3 種線:
細(xì)橫線:內(nèi)存分配大小刻度線
藍(lán)色豎線:表示內(nèi)存在對(duì)應(yīng)時(shí)刻被分配,最后仍然活躍
灰色豎線:表示內(nèi)存在對(duì)應(yīng)時(shí)刻被分配,但最后被回收
時(shí)間軸的幾個(gè)操作:
鼠標(biāo)移動(dòng)到時(shí)間軸內(nèi)任意位置,點(diǎn)擊左鍵或長(zhǎng)按左鍵并拖動(dòng)即可選擇一段時(shí)間
鼠標(biāo)拖動(dòng)時(shí)間段框上方的方塊可以對(duì)已選擇的時(shí)間段進(jìn)行調(diào)整
鼠標(biāo)移到已選擇的時(shí)間段框內(nèi)部,滑動(dòng)滾輪可以調(diào)整時(shí)間范圍
鼠標(biāo)移到已選擇的時(shí)間段框兩旁,滑動(dòng)滾輪即可調(diào)整時(shí)間段
雙擊鼠標(biāo)左鍵即可取消選擇
在時(shí)間軸中選擇要查看的時(shí)間段,即可得到該段時(shí)間的內(nèi)存分配詳情。
分配時(shí)間軸的包含視圖與堆快照的包含視圖是一樣的,這里就不再重復(fù)介紹了。
對(duì)不起各位,這玩意兒我也不知道有啥用…
打開(kāi)就直接報(bào)錯(cuò),我:喵喵喵?
是不是因?yàn)闆](méi)人用這玩意兒,所以沒(méi)人發(fā)現(xiàn)有問(wèn)題…
分配時(shí)間軸的統(tǒng)計(jì)視圖與堆快照的統(tǒng)計(jì)視圖也是一樣的,不再贅述。
Memory 面板上的簡(jiǎn)介:使用采樣方法記錄內(nèi)存分配。這種分析方式的性能開(kāi)銷最小,可以用于長(zhǎng)時(shí)間的記錄。
好家伙,這個(gè)簡(jiǎn)介有夠模糊,說(shuō)了跟沒(méi)說(shuō)似的,很有精神!
我在官方文檔里沒(méi)有找到任何關(guān)于分配采樣的介紹,Google 上也幾乎沒(méi)有與之有關(guān)的信息。所以以下內(nèi)容僅為個(gè)人實(shí)踐得出的結(jié)果,如有不對(duì)的地方歡迎各位指出!
簡(jiǎn)單來(lái)說(shuō),通過(guò)分配采樣我們可以很直觀地看到代碼中的每個(gè)函數(shù)(API)所分配的內(nèi)存大小。
由于是采樣的方式,所以結(jié)果并非百分百準(zhǔn)確,即使每次執(zhí)行相同的操作也可能會(huì)有不同的結(jié)果,但是足以讓我們了解內(nèi)存分配的大體情況。
? 如何開(kāi)始
點(diǎn)擊頁(yè)面底部的 Start 按鈕或者左上角的 ? 按鈕即可開(kāi)始記錄,記錄過(guò)程中點(diǎn)擊左上角的 ? 按鈕來(lái)結(jié)束記錄,片刻之后就會(huì)自動(dòng)展示結(jié)果。
? 操作一下
① 打開(kāi) Memory 面板,開(kāi)始記錄分配采樣。
② 切換到 Console 面板,執(zhí)行以下代碼:
代碼看起來(lái)有點(diǎn)長(zhǎng),其實(shí)就是 4 個(gè)函數(shù)分別以不同的方式往數(shù)組里面添加對(duì)象。
// 普通單層調(diào)用 let array_a = []; function aoo1() { for (let i = 0; i < 10000; i++) { array_a.push({ a: 'pp' }); } } aoo1(); // 兩層嵌套調(diào)用 let array_b = []; function boo1() { function boo2() { for (let i = 0; i < 20000; i++) { array_b.push({ b: 'pp' }); } } boo2(); } boo1(); // 三層嵌套調(diào)用 let array_c = []; function coo1() { function coo2() { function coo3() { for (let i = 0; i < 30000; i++) { array_c.push({ c: 'pp' }); } } coo3(); } coo2(); } coo1(); // 兩層嵌套多個(gè)調(diào)用 let array_d = []; function doo1() { function doo2_1() { for (let i = 0; i < 20000; i++) { array_d.push({ d: 'pp' }); } } doo2_1(); function doo2_2() { for (let i = 0; i < 20000; i++) { array_d.push({ d: 'pp' }); } } doo2_2(); } doo1();
③ 切換回 Memory 面板,停止記錄,片刻之后會(huì)自動(dòng)進(jìn)入結(jié)果頁(yè)面。
分配采樣結(jié)果頁(yè)有 3 種視圖可選:
Chart:圖表視圖
Heavy (Bottom Up):扁平視圖(調(diào)用層級(jí)自下而上)
Tree (Top Down):樹(shù)狀視圖(調(diào)用層級(jí)自上而下)
這個(gè) Heavy 我真的不知道該怎么翻譯,所以我就按照具體表現(xiàn)來(lái)命名了。
默認(rèn)會(huì)顯示 Chart 視圖。
Chart 視圖以圖形化的表格形式展現(xiàn)各個(gè)函數(shù)的內(nèi)存分配詳情,可以選擇精確到內(nèi)存分配的不同階段(以內(nèi)存分配的大小為軸)。
鼠標(biāo)左鍵點(diǎn)擊、拖動(dòng)和雙擊以操作內(nèi)存分配階段軸(和時(shí)間軸一樣),選擇要查看的階段范圍。
將鼠標(biāo)移動(dòng)到函數(shù)方塊上會(huì)顯示函數(shù)的內(nèi)存分配詳情。
鼠標(biāo)左鍵點(diǎn)擊函數(shù)方塊可以跳轉(zhuǎn)到相應(yīng)代碼。
Heavy 視圖將函數(shù)調(diào)用層級(jí)壓平,函數(shù)將以獨(dú)立的個(gè)體形式展現(xiàn)。另外也可以展開(kāi)調(diào)用層級(jí),不過(guò)是自下而上的結(jié)構(gòu),也就是一個(gè)反向的函數(shù)調(diào)用過(guò)程。
視圖中的兩種 Size(大?。?/p>
Self Size:自身大小,指的是在函數(shù)內(nèi)部直接分配的內(nèi)存空間大小。
Total Size:總大小,指的是函數(shù)總共分配的內(nèi)存空間大小,也就是包括函數(shù)內(nèi)部嵌套調(diào)用的其他函數(shù)所分配的大小。
Tree 視圖以樹(shù)形結(jié)構(gòu)展現(xiàn)函數(shù)調(diào)用層級(jí)。我們可以從代碼執(zhí)行的源頭開(kāi)始自上而下逐層展開(kāi),呈現(xiàn)一個(gè)完整的正向的函數(shù)調(diào)用過(guò)程。
關(guān)于“javascript中內(nèi)存指的是什么”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對(duì)“javascript中內(nèi)存指的是什么”知識(shí)都有一定的了解,大家如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。