<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          JavaScript 內(nèi)存詳解 & 分析指南

          共 13687字,需瀏覽 28分鐘

           ·

          2021-01-13 22:08

          前言

          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ì)于這門(mén)語(yǔ)言并沒(méi)有更多的了解。

          如果想要成為一名更好的 JavaScript 開(kāi)發(fā)者,理解內(nèi)存是一個(gè)不可忽略的關(guān)鍵點(diǎn)。

          ?? 本文主要包含兩大部分:

          1. JavaScript 內(nèi)存詳解
          2. JavaScript 內(nèi)存分析指南

          看完這篇文章后,相信你會(huì)對(duì) JavaScript 的內(nèi)存有比較全面的了解,并且能夠擁有獨(dú)自進(jìn)行內(nèi)存分析的能力。

          ?? 話(huà)不多說(shuō),我們開(kāi)始吧!

          文章篇幅較長(zhǎng),除去代碼也有 12000 字左右,需要一定的時(shí)間來(lái)閱讀,但是我保證你所花費(fèi)的時(shí)間都是值得的。


          正文

          內(nèi)存(memory)

          什么是內(nèi)存(What is memory)

          相信大家都對(duì)內(nèi)存有一定的了解,我就不從盤(pán)古開(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)兩大類(lèi)。

          在速度上 SRAM 要遠(yuǎn)快于 DRAM,而 SRAM 的速度僅次于 CPU 內(nèi)部的寄存器。

          在現(xiàn)代計(jì)算機(jī)中,高速緩存使用的是 SRAM,而主存儲(chǔ)器使用的是 DRAM。

          ?? 主存儲(chǔ)器(Main memory,主存)

          雖然高速緩存的速度很快,但是其存儲(chǔ)容量很小,小到幾 KB 最大也才幾十 MB,根本不足以?xún)?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ō)這么多,想要深入了解的話(huà)可以根據(jù)上面提到關(guān)鍵詞進(jìn)行搜索。

          ?? 軟件層面(Software)

          在軟件層面上,內(nèi)存通常指的是操作系統(tǒng)從主存中劃分(抽象)出來(lái)的內(nèi)存空間。

          此時(shí)內(nèi)存又可以分為兩類(lèi):棧內(nèi)存和堆內(nèi)存。

          接下來(lái)我將圍繞 JavaScript 這門(mén)語(yǔ)言來(lái)對(duì)內(nèi)存進(jìn)行講解。

          在后面的文章中所提到的內(nèi)存均指軟件層面上的內(nèi)存。

          棧與堆(Stack & Heap)

          棧內(nèi)存(Stack memory)

          ?? 棧(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)存的訪(fǎng)問(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)存的大小

          棧內(nèi)存由操作系統(tǒng)直接管理,所以棧內(nèi)存的大小也由操作系統(tǒng)決定。

          通常來(lái)說(shuō),每一條線(xiàn)程(Thread)都會(huì)有獨(dú)立的棧內(nèi)存空間,Windows 給每條線(xiàn)程分配的棧內(nèi)存默認(rèn)大小為 1MB。

          堆內(nèi)存(Heap memory)

          ?? 堆(Heap)

          堆也是一種常見(jiàn)的數(shù)據(jù)結(jié)構(gòu),但是不在本文討論范圍內(nèi),就不多說(shuō)了。

          堆內(nèi)存雖然名字里有個(gè)“堆”字,但是它和數(shù)據(jù)結(jié)構(gòu)中的堆沒(méi)半毛錢(qián)關(guān)系,就只是撞了名罷了。

          堆內(nèi)存是一大片內(nèi)存空間,堆內(nèi)存的分配是動(dòng)態(tài)且不連續(xù)的,程序可以按需申請(qǐng)堆內(nèi)存空間,但是訪(fǎng)問(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)存

          ?? 堆內(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)程、線(xiàn)程與堆內(nèi)存

          通常來(lái)說(shuō),一個(gè)進(jìn)程(Process)只會(huì)有一個(gè)堆內(nèi)存,同一進(jìn)程下的多個(gè)線(xiàn)程會(huì)共享同一個(gè)堆內(nèi)存。

          在 Chrome 瀏覽器中,一般情況下每個(gè)標(biāo)簽頁(yè)都有單獨(dú)的進(jìn)程,不過(guò)在某些情況下也會(huì)出現(xiàn)多個(gè)標(biāo)簽頁(yè)共享一個(gè)進(jìn)程的情況。

          函數(shù)調(diào)用(Function calling)

          明白了棧內(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é)束。

          函數(shù)調(diào)用過(guò)程

          上圖中的內(nèi)容經(jīng)過(guò)了簡(jiǎn)化,剝離了棧幀和各種指針的概念,主要展示函數(shù)調(diào)用以及內(nèi)存分配的大概過(guò)程。

          在同一線(xiàn)程下(JavaScript 是單線(xiàn)程的),所有被執(zhí)行的函數(shù)以及函數(shù)的參數(shù)和局部變量都會(huì)被推入到同一個(gè)棧內(nèi)存中,這也就是大量遞歸會(huì)導(dǎo)致棧溢出(Stack overflow)的原因。

          關(guān)于圖中涉及到的函數(shù)內(nèi)部變量?jī)?nèi)存分配的詳情請(qǐng)接著往下看。

          儲(chǔ)存變量(Store variables)

          當(dāng) JavaScript 程序運(yùn)行時(shí),在非全局作用域中產(chǎn)生的局部變量均儲(chǔ)存在棧內(nèi)存中。

          但是,只有原始類(lèi)型的變量是真正地把值儲(chǔ)存在棧內(nèi)存中。

          而引用類(lèi)型的變量只在棧內(nèi)存中儲(chǔ)存一個(gè)引用(reference),這個(gè)引用指向堆內(nèi)存里的真正的值。

          ?? 原始類(lèi)型(Primitive type)

          原始類(lèi)型又稱(chēng)基本類(lèi)型,包括 stringnumberbigintbooleanundefinednullsymbol(ES6 新增)。

          原始類(lèi)型的值被稱(chēng)為原始值(Primitive value)。

          補(bǔ)充:雖然 typeof null 返回的是 'object',但是 null 真的不是對(duì)象,會(huì)出現(xiàn)這樣的結(jié)果其實(shí)是 JavaScript 的一個(gè) Bug~

          ?? 引用類(lèi)型(Reference type)

          除了原始類(lèi)型外,其余類(lèi)型都屬于引用類(lèi)型,包括 ObjectArrayFunctionDateRegExpStringNumberBoolean 等等...

          實(shí)際上 Object 是最基本的引用類(lèi)型,其他引用類(lèi)型均繼承自 Object。也就是說(shuō),所有引用類(lèi)型的值實(shí)際上都是對(duì)象。

          引用類(lèi)型的值被稱(chēng)為引用值(Reference value)。

          ?? 簡(jiǎn)單來(lái)說(shuō)

          在多數(shù)情況下,原始類(lèi)型的數(shù)據(jù)儲(chǔ)存在棧內(nèi)存,而引用類(lèi)型的數(shù)據(jù)(對(duì)象)則儲(chǔ)存在堆內(nèi)存。

          變量的儲(chǔ)存
          特別注意(Attention)

          全局變量以及被閉包引用的變量(即使是原始類(lè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ī)制。

          不可變與可變(Immutable and Mutable)

          棧內(nèi)存中會(huì)儲(chǔ)存兩種變量數(shù)據(jù):原始值和對(duì)象引用。

          不僅類(lèi)型不同,它們?cè)跅?nèi)存中的具體表現(xiàn)也不太一樣。

          原始值(Primitive values)

          ?? Primitive values are immutable!

          前面有說(shuō)到:原始類(lèi)型的數(shù)據(jù)(原始值)直接儲(chǔ)存在棧內(nèi)存中。

          當(dāng)我們定義一個(gè)原始類(lèi)型變量的時(shí)候,JavaScript 會(huì)在棧內(nèi)存中激活一塊內(nèi)存來(lái)儲(chǔ)存變量的值(原始值)

          當(dāng)我們更改原始類(lèi)型變量的值時(shí),實(shí)際上會(huì)再激活一塊新的內(nèi)存來(lái)儲(chǔ)存新的值,并將變量指向新的內(nèi)存空間,而不是改變?cè)瓉?lái)那塊內(nèi)存里的值。

          當(dāng)我們將一個(gè)原始類(lèi)型變量賦值給另一個(gè)新的變量(也就是復(fù)制變量)時(shí),也是會(huì)再激活一塊新的內(nèi)存,并將源變量?jī)?nèi)存里的值復(fù)制一份到新的內(nèi)存里

          更改原始類(lèi)型變量

          ?? 總之就是:棧內(nèi)存中的原始值一旦確定就不能被更改(不可變的)。

          原始值的比較(Comparison)

          當(dāng)我們比較原始類(lèi)型的變量時(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
          對(duì)象引用(Object references)

          ?? Object references are mutable!

          前面也有說(shuō)到:引用類(lèi)型的變量在棧內(nèi)存中儲(chǔ)存的只是一個(gè)指向堆內(nèi)存的引用。

          當(dāng)我們定義一個(gè)引用類(lèi)型的變量時(shí),JavaScript 會(huì)先在堆內(nèi)存中找到一塊合適的地方來(lái)儲(chǔ)存對(duì)象,并激活一塊棧內(nèi)存來(lái)儲(chǔ)存對(duì)象的引用(堆內(nèi)存地址),最后將變量指向這塊棧內(nèi)存

          ?? 所以當(dāng)我們通過(guò)變量訪(fǎng)問(wèn)對(duì)象時(shí),實(shí)際的訪(fǎng)問(wèn)過(guò)程應(yīng)該是:

          變量 -> 棧內(nèi)存中的引用 -> 堆內(nèi)存中的值

          當(dāng)我們把引用類(lèi)型變量賦值給另一個(gè)變量時(shí),會(huì)將源變量指向的棧內(nèi)存中的對(duì)象引用復(fù)制到新變量的棧內(nèi)存中,所以實(shí)際上只是復(fù)制了個(gè)對(duì)象引用,并沒(méi)有在堆內(nèi)存中生成一份新的對(duì)象。

          而當(dāng)我們給引用類(lèi)型變量分配為一個(gè)新的對(duì)象時(shí),則會(huì)直接修改變量指向的棧內(nèi)存中的引用,新的引用指向堆內(nèi)存中新的對(duì)象。

          更改引用類(lèi)型變量

          ?? 總之就是:棧內(nèi)存中的對(duì)象引用是可以被更改的(可變的)。

          對(duì)象的比較(Comparison)

          所有引用類(lèi)型的值實(shí)際上都是對(duì)象。

          當(dāng)我們比較引用類(lèi)型的變量時(shí),實(shí)際上是在比較棧內(nèi)存中的引用,只有引用相同時(shí)變量才相等。

          即使是看起來(lái)完全一樣的兩個(gè)引用類(lèi)型變量,只要他們的引用的不是同一個(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
          對(duì)象的深拷貝(Deep copy)

          當(dāng)我們搞明白引用類(lèi)型變量在內(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ò)去;如果遇到引用類(lèi)型的屬性,那就再新建一個(gè)對(duì)象,繼續(xù)復(fù)制...

          此時(shí)我們就需要借助遞歸來(lái)實(shí)現(xiàn)多層次對(duì)象的復(fù)制,這也就是我們說(shuō)的深拷貝。

          對(duì)于任何引用類(lèi)型的變量,都應(yīng)該使用深拷貝來(lái)復(fù)制,除非你很確定你的目的就是復(fù)制一個(gè)引用。

          內(nèi)存生命周期(Memory life cycle)

          通常來(lái)說(shuō),所有應(yīng)用程序的內(nèi)存生命周期都是基本一致的:

          分配 -> 使用 -> 釋放

          當(dāng)我們使用高級(jí)語(yǔ)言編寫(xiě)程序時(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 編寫(xiě)的)。

          但是這不意味著我們就不需要在乎內(nèi)存管理,了解內(nèi)存的更多細(xì)節(jié)可以幫助我們寫(xiě)出性能更好,穩(wěn)定性更高的代碼。

          垃圾回收(Garbage collection)

          垃圾回收即我們常說(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ì)象訪(fǎng)問(wèn)到,如果可以那么該變量就是可達(dá)的(Reachable),否則就是不可達(dá)的(Unreachable)。

          可達(dá)與不可達(dá)

          上圖中的節(jié)點(diǎn) 9 和節(jié)點(diǎn) 10 均無(wú)法通過(guò)節(jié)點(diǎn) 1(根節(jié)點(diǎn))直接或間接訪(fǎng)問(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)致程序的崩潰。

          垃圾回收算法(Algorithms)

          垃圾回收的基本思路很簡(jiǎn)單:確定哪個(gè)變量不會(huì)再使用,然后釋放它占用的內(nèi)存。

          實(shí)際上,在回收過(guò)程中想要確定一個(gè)變量是否還有用并不簡(jiǎn)單。

          直到現(xiàn)在也還沒(méi)有一個(gè)真正完美的垃圾回收算法,接下來(lái)介紹 3 種最廣為人知的垃圾回收算法。

          標(biāo)記-清除(Mark-and-Sweep)

          標(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)烈建議你抽空專(zhuān)門(mén)去學(xué)習(xí)下!!!

          垃圾回收器將定期掃描內(nèi)存中的所有變量,將處于上下文中以及被處于上下文中的變量引用的變量的標(biāo)記去除,將其余變量標(biāo)記為“待刪除”。

          隨后,垃圾回收器會(huì)清除所有帶有“待刪除”標(biāo)記的變量,并釋放它們所占用的內(nèi)存。

          標(biāo)記-整理(Mark-Compact)

          準(zhǔn)確來(lái)說(shuō),Compact 應(yīng)譯為緊湊、壓縮,但是在這里我覺(jué)得用“整理”更為貼切。

          標(biāo)記整理算法也是常用的垃圾收集算法之一。

          使用標(biāo)記整理算法可以解決內(nèi)存碎片化的問(wèn)題(通過(guò)整理),提高內(nèi)存空間的可用性。

          但是,該算法的標(biāo)記階段比較耗時(shí),可能會(huì)堵塞主線(xiàn)程,導(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)始,一層一層往下查詢(xún),直到標(biāo)記完所有活躍的對(duì)象,那么剩下的未被標(biāo)記的對(duì)象就是不可達(dá)的了。

          V8 的標(biāo)記階段

          然后是整理階段(碎片整理),垃圾回收器會(huì)將活躍的(被標(biāo)記了的)對(duì)象往內(nèi)存空間的一端移動(dòng),這個(gè)過(guò)程可能會(huì)改變內(nèi)存中的對(duì)象的內(nèi)存地址。

          最后來(lái)到清除階段,垃圾回收器會(huì)將邊界后面(也就是最后一個(gè)活躍的對(duì)象后面)的對(duì)象清除,并釋放它們占用的內(nèi)存空間。

          V8 的標(biāo)記整理算法
          引用計(jì)數(shù)(Reference counting)

          引用計(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)稱(chēng)為?ππ
          //?而?name?屬性為?pp?的對(duì)象則簡(jiǎn)稱(chēng)為?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ì)象 ab 都已經(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)存泄露了!

          V8 中的垃圾回收(GC in V8)

          8?? V8

          V8 是一個(gè)由 Google 開(kāi)源的用 C++ 編寫(xiě)的高性能 JavaScript 引擎。

          V8 是目前最流行的 JavaScript 引擎之一,我們熟知的 Chrome 瀏覽器和 Node.js 等軟件都在使用 V8。

          在 V8 的內(nèi)存管理機(jī)制中,把堆內(nèi)存(Heap memory)劃分成了多個(gè)區(qū)域。

          V8 常駐集

          這里我們只關(guān)注這兩個(gè)區(qū)域:

          • New Space(新空間):又稱(chēng) Young generation(新世代),用于儲(chǔ)存新生成的對(duì)象,由 Minor GC 進(jìn)行管理。
          • Old Space(舊空間):又稱(chēng) 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)。

          為了提升用戶(hù)體驗(yàn),V8 還實(shí)現(xiàn)了一個(gè)名為增量標(biāo)記(Incremental marking)的特性。

          增量標(biāo)記的要點(diǎn)就是把標(biāo)記工作分成多個(gè)小段,夾雜在主線(xiàn)程(Main thread)的 JavaScript 邏輯中,這樣就不會(huì)長(zhǎng)時(shí)間阻塞主線(xiàn)程了。

          增量標(biāo)記

          當(dāng)然增量標(biāo)記也有代價(jià)的,在增量標(biāo)記過(guò)程中所有對(duì)象的變化都需要通知垃圾回收器,好讓垃圾回收器能夠正確地標(biāo)記那些對(duì)象,這里的“通知”也是需要成本的。

          另外 V8 中還有使用工作線(xiàn)程(Worker thread)實(shí)現(xiàn)的平行標(biāo)記(Parallel marking)和并行標(biāo)記(Concurrent marking),這里我就不再細(xì)說(shuō)了~

          ?? 總結(jié)一下

          為了提升性能和用戶(hù)體驗(yàn),V8 內(nèi)部做了非常非常多的“騷操作”,本文提到的都只是冰山一角,但足以讓我五體投地佩服連連!

          總之就是非常 Amazing 啊~

          內(nèi)存管理(Memory management)

          或者說(shuō)是:內(nèi)存優(yōu)化(Memory optimization)?

          雖然我們寫(xiě)代碼的時(shí)候一般不會(huì)直接接觸內(nèi)存管理,但是有一些注意事項(xiàng)可以讓我們避免引起內(nèi)存問(wèn)題,甚至提升代碼的性能。

          全局變量(Global variable)

          全局變量的訪(fǎng)問(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ì)象直接訪(fǎng)問(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;

          隱藏類(lèi)(HiddenClass)

          實(shí)際上的隱藏類(lèi)遠(yuǎn)比本文所提到的復(fù)雜,但是今天的主角不是它,所以我們點(diǎn)到為止。

          在 V8 內(nèi)部有一個(gè)叫做“隱藏類(lèi)”的機(jī)制,主要用于提升對(duì)象(Object)的性能。

          V8 里的每一個(gè) JS 對(duì)象(JS Objects)都會(huì)關(guān)聯(lián)一個(gè)隱藏類(lèi),隱藏類(lèi)里面儲(chǔ)存了對(duì)象的形狀(特征)和屬性名稱(chēng)到屬性的映射等信息。

          隱藏類(lèi)內(nèi)記錄了每個(gè)屬性的內(nèi)存偏移(Memory offset),后續(xù)訪(fǎng)問(wèn)屬性的時(shí)候就可以快速定位到對(duì)應(yīng)屬性的內(nèi)存位置,從而提升對(duì)象屬性的訪(fǎng)問(wèn)速度。

          在我們創(chuàng)建對(duì)象時(shí),擁有完全相同的特征(相同屬性且相同順序)的對(duì)象可以共享同一個(gè)隱藏類(lèi)。

          ?? 再想象一下

          我們可以把隱藏類(lèi)想象成工業(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è)隱藏類(lèi)
          //?good!

          隨后在 Memory 面板打一個(gè)堆快照,通過(guò)堆快照中的 Comparison 視圖可以快速找到上面創(chuàng)建的 3 個(gè)對(duì)象:

          注:關(guān)于如何查看內(nèi)存中的對(duì)象將會(huì)在文章的第二大部分中進(jìn)行講解,現(xiàn)在讓我們專(zhuān)注于隱藏類(lèi)。

          隱藏類(lèi)示例

          在上圖中可以很清楚地看到對(duì)象 A 和 B 確實(shí)使用了同一個(gè)隱藏類(lèi)。

          而對(duì)象 C 因?yàn)槎嗔艘粋€(gè) gender 屬性,所以不能和前面兩個(gè)對(duì)象共享隱藏類(lèi)。

          ?? 動(dòng)態(tài)增刪對(duì)象屬性

          一般情況下,當(dāng)我們動(dòng)態(tài)修改對(duì)象的特征(增刪屬性)時(shí),V8 會(huì)為該對(duì)象分配一個(gè)能用的隱藏類(lèi)或者創(chuàng)建一個(gè)新的隱藏類(lèi)(新的分支)。

          例如動(dòng)態(tài)地給對(duì)象增加一個(gè)新的屬性:

          注:這種操作被稱(chēng)為“先創(chuàng)建再補(bǔ)充(ready-fire-aim)”。

          //?增加?gender?屬性
          objectB.gender?=?'男';
          //?對(duì)象?B?的特征發(fā)生了變化
          //?多了一個(gè)原本沒(méi)有的?gender?屬性
          //?導(dǎo)致對(duì)象?B?不能再與?A?共享隱藏類(lèi)
          //?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ù)字組引屬性';
          //?不影響共享隱藏類(lèi)
          //?so?far?so?good!

          ?? 那問(wèn)題來(lái)了

          說(shuō)了這么多,隱藏類(lèi)看起來(lái)確實(shí)可以提升性能,那它和內(nèi)存又有什么關(guān)系呢?

          實(shí)際上,隱藏類(lèi)也需要占用內(nèi)存空間,這其實(shí)就是一種用空間換時(shí)間的機(jī)制。

          如果由于動(dòng)態(tài)增刪對(duì)象屬性而創(chuàng)建了大量隱藏類(lèi)和分支,結(jié)果就是會(huì)浪費(fèi)不少內(nèi)存空間。

          ?? 舉個(gè)栗子

          創(chuàng)建 1000 個(gè)擁有相同屬性的對(duì)象,內(nèi)存中只會(huì)多出 1 個(gè)隱藏類(lèi)。

          而創(chuàng)建 1000 個(gè)屬性信息完全不同的對(duì)象,內(nèi)存中就會(huì)多出 1000 個(gè)隱藏類(lèi)。

          ?? 應(yīng)該怎么做?

          所以,我們要盡量避免動(dòng)態(tài)增刪對(duì)象屬性操作,應(yīng)該在構(gòu)造函數(shù)內(nèi)就一次性聲明所有需要用到的屬性。

          如果確實(shí)不再需要某個(gè)屬性,我們可以將屬性的值設(shè)為 null,如下:

          //?將?age?屬性置空
          objectB.age?=?null;
          //?still?good!

          另外,相同名稱(chēng)的屬性盡量按照相同的順序來(lái)聲明,可以盡可能地讓更多對(duì)象共享相同的隱藏類(lèi)。

          即使遇到不能共享隱藏類(lèi)的情況,也至少可以減少隱藏類(lèi)分支的產(chǎn)生。

          其實(shí)動(dòng)態(tài)增刪對(duì)象屬性所引起的性能問(wèn)題更為關(guān)鍵,但因本文篇幅有限,就不再展開(kāi)了。

          閉包(Closure)

          前面有提到:被閉包引用的變量?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è)作品稱(chēng)為:閉包炸彈
          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?終于得救了

          如何分析內(nèi)存(Analyze)

          說(shuō)了這么多,那我們應(yīng)該如何查看并分析程序運(yùn)行時(shí)的內(nèi)存情況呢?

          “工欲善其事,必先利其器。”

          對(duì)于 Web 前端項(xiàng)目來(lái)說(shuō),分析內(nèi)存的最佳工具非 Memory 莫屬!

          這里的 Memory 指的是 DevTools 中的一個(gè)工具,為了避免混淆,下面我會(huì)用“Memory 面板”或”內(nèi)存面板“代稱(chēng)。

          ?? DevTools(開(kāi)發(fā)者工具)

          DevTools 是瀏覽器里內(nèi)置的一套用于 Web 開(kāi)發(fā)和調(diào)試的工具。

          使用 Chromuim 內(nèi)核的瀏覽器都帶有 DevTools,個(gè)人推薦使用 Chrome 或者 Edge(新)。

          Memory in Devtools(內(nèi)存面板)

          在我們切換到 Memory 面板后,會(huì)看到以下界面(注意標(biāo)注):

          Memory 面板

          在這個(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(淺層大小)

          淺層大小指的是當(dāng)前對(duì)象自身占用的內(nèi)存大小。

          淺層大小不包含自身引用的對(duì)象。

          ?? Retained Size(保留大小)

          保留大小指的是當(dāng)前對(duì)象被 GC 回收后總共能夠釋放的內(nèi)存大小。

          換句話(huà)說(shuō),也就是當(dāng)前對(duì)象自身大小加上對(duì)象直接或間接引用的其他對(duì)象的大小總和。

          需要注意的是,保留大小不包含那些除了被當(dāng)前對(duì)象引用之外還被全局對(duì)象直接或間接引用的對(duì)象。

          Heap snapshot(堆快照)

          分析類(lèi)型-堆快照

          堆快照可以記錄頁(yè)面當(dāng)前時(shí)刻的 JS 對(duì)象以及 DOM 節(jié)點(diǎn)的內(nèi)存分配情況。

          ?? 如何開(kāi)始

          點(diǎn)擊頁(yè)面底部的 Take snapshot 按鈕或者左上角的 ? 按鈕即可打一個(gè)堆快照,片刻之后就會(huì)自動(dòng)展示結(jié)果。

          選擇一個(gè)視圖

          在堆快照結(jié)果頁(yè)面中,我們可以使用 4 種不同的視圖來(lái)觀察內(nèi)存情況:

          • Summary:摘要視圖
          • Comparison:比較視圖
          • Containment:包含視圖
          • Statistics:統(tǒng)計(jì)視圖

          默認(rèn)顯示 Summary 視圖。

          Summary(摘要視圖)

          摘要視圖根據(jù) Constructor(構(gòu)造函數(shù))來(lái)將對(duì)象進(jìn)行分組,我們可以在 Class filter(類(lèi)過(guò)濾器)中輸入構(gòu)造函數(shù)名稱(chēng)來(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)類(lèi)型(ArrayStringNumberSymbolRegExp)的數(shù)據(jù)。
          • (concatenated string):使用 concat() 函數(shù)拼接而成的字符串。
          • (sliced string):使用 slice()substring() 等函數(shù)進(jìn)行邊緣切割的字符串。
          • (system):系統(tǒng)(引擎)產(chǎn)生的對(duì)象,如 V8 創(chuàng)建的 HiddenClasses(隱藏類(lèi))和 DescriptorArrays(描述符數(shù)組)等數(shù)據(jù)。

          ?? DescriptorArrays(描述符數(shù)組)

          描述符數(shù)組主要包含對(duì)象的屬性名信息,是隱藏類(lèi)的重要組成部分。

          不過(guò)描述符數(shù)組內(nèi)不會(huì)包含整數(shù)索引屬性。

          而其余沒(méi)有用“()”包裹的則為全局屬性和 GC 根。

          另外,每個(gè)對(duì)象后面都會(huì)有一串“@”開(kāi)頭的數(shù)字,這是對(duì)象在內(nèi)存中的唯一 ID。

          小貼士:按下快捷鍵 Ctrl/Command + F 展示搜索欄,輸入名稱(chēng)或 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();
          實(shí)例化一個(gè)對(duì)象

          回到 Memory 面板,打一個(gè)堆快照,在 Class filter 中輸入“TestClass”:

          可以看到內(nèi)存中有一個(gè) TestClass 的實(shí)例,該實(shí)例的淺層大小為 80 字節(jié),保留大小為 876 字節(jié)。

          內(nèi)存中的對(duì)象實(shí)例

          ?? 注意到了嗎?

          堆快照中的 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)并輸入“我是吳彥祖”:

          內(nèi)存中的吳彥祖
          Comparison(比較視圖)

          只有同時(shí)存在 2 個(gè)或以上的堆快照時(shí)才會(huì)出現(xiàn) Comparison 選項(xiàng)。

          比較視圖用于展示兩個(gè)堆快照之間的差異。

          使用比較視圖可以讓我們快速得知在執(zhí)行某個(gè)操作后的內(nèi)存變化情況(如新增或減少對(duì)象)。

          通過(guò)多個(gè)快照的對(duì)比還可以讓我們快速判斷并定位內(nèi)存泄漏。

          文章前面提到隱藏類(lè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

          比較視圖-新增實(shí)例

          再次切換到 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)存空間。

          比較視圖-實(shí)例對(duì)象
          Containment(包含視圖)

          包含視圖就是程序?qū)ο蠼Y(jié)構(gòu)的“鳥(niǎo)瞰圖(Bird's eye view)”,允許我們通過(guò)全局對(duì)象出發(fā),一層一層往下探索,從而了解內(nèi)存的詳細(xì)情況。

          堆快照-統(tǒng)計(jì)視圖

          包含視圖中有以下幾種全局對(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 線(xiàn)程堆棧)、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ì)象,包括 ObjectFunctionArrayStringBooleanNumberDateRegExpMath 等對(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

          包含視圖-示例
          Statistics(統(tǒng)計(jì)視圖)

          統(tǒng)計(jì)視圖可以很直觀地展示內(nèi)存整體分配情況。

          堆快照-統(tǒng)計(jì)視圖

          在該視圖里的空心餅圖中共有 6 種顏色,各含義分別為:

          • 紅色:Code(代碼)
          • 綠色:Strings(字符串)
          • 藍(lán)色:JS arrays(數(shù)組)
          • 橙色:Typed arrays(類(lèi)型化數(shù)組)
          • 紫色:System objects(系統(tǒng)對(duì)象)
          • 白色:空閑內(nèi)存

          Allocation instrumentation on timeline(分配時(shí)間軸)

          分析類(lèi)型-分配時(shí)間軸

          在一段時(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í)間軸-視圖模式

          分配時(shí)間軸結(jié)果頁(yè)有 4 種視圖:

          • Summary:摘要視圖
          • Containment:包含視圖
          • Allocation:分配視圖
          • Statistics:統(tǒng)計(jì)視圖

          默認(rèn)顯示 Summary 視圖。

          Summary(摘要視圖)

          看起來(lái)和堆快照的摘要視圖很相似,主要是頁(yè)面上方多了一條橫向的時(shí)間軸(Timeline)。

          分配時(shí)間軸-摘要視圖

          ?? 時(shí)間軸

          時(shí)間軸中主要的 3 種線(xiàn):

          • 細(xì)橫線(xiàn):內(nèi)存分配大小刻度線(xiàn)
          • 藍(lán)色豎線(xiàn):表示內(nèi)存在對(duì)應(yīng)時(shí)刻被分配,最后仍然活躍
          • 灰色豎線(xiàn):表示內(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í)間軸中選擇要查看的時(shí)間段,即可得到該段時(shí)間的內(nèi)存分配詳情。

          分配時(shí)間軸-摘要視圖
          Containment(包含視圖)

          分配時(shí)間軸的包含視圖與堆快照的包含視圖是一樣的,這里就不再重復(fù)介紹了。

          分配時(shí)間軸-包含視圖
          Allocation(分配視圖)

          對(duì)不起各位,這玩意兒我也不知道有啥用...

          打開(kāi)就直接報(bào)錯(cuò),我:喵喵喵?

          分配時(shí)間軸-分配視圖

          是不是因?yàn)闆](méi)人用這玩意兒,所以沒(méi)人發(fā)現(xiàn)有問(wèn)題...

          Statistics(統(tǒng)計(jì)視圖)

          分配時(shí)間軸的統(tǒng)計(jì)視圖與堆快照的統(tǒng)計(jì)視圖也是一樣的,不再贅述。

          分配時(shí)間軸-統(tǒng)計(jì)視圖

          Allocation sampling(分配采樣)

          分析類(lèi)型-分配采樣

          Memory 面板上的簡(jiǎn)介:使用采樣方法記錄內(nèi)存分配。這種分析方式的性能開(kāi)銷(xiāo)最小,可以用于長(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(圖表視圖)

          Chart 視圖以圖形化的表格形式展現(xiàn)各個(gè)函數(shù)的內(nèi)存分配詳情,可以選擇精確到內(nèi)存分配的不同階段(以?xún)?nèi)存分配的大小為軸)。

          分配采樣-圖表視圖

          鼠標(biāo)左鍵點(diǎn)擊、拖動(dòng)和雙擊以操作內(nèi)存分配階段軸(和時(shí)間軸一樣),選擇要查看的階段范圍。

          分配采樣-操作階段軸

          將鼠標(biāo)移動(dòng)到函數(shù)方塊上會(huì)顯示函數(shù)的內(nèi)存分配詳情。

          顯示內(nèi)存分配詳情

          鼠標(biāo)左鍵點(diǎn)擊函數(shù)方塊可以跳轉(zhuǎn)到相應(yīng)代碼。

          跳轉(zhuǎn)到相應(yīng)代碼
          Heavy(扁平視圖)

          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(大小):

          • Self Size:自身大小,指的是在函數(shù)內(nèi)部直接分配的內(nèi)存空間大小。
          • Total Size:總大小,指的是函數(shù)總共分配的內(nèi)存空間大小,也就是包括函數(shù)內(nèi)部嵌套調(diào)用的其他函數(shù)所分配的大小。
          Tree(樹(shù)狀視圖)

          Tree 視圖以樹(shù)形結(jié)構(gòu)展現(xiàn)函數(shù)調(diào)用層級(jí)。我們可以從代碼執(zhí)行的源頭開(kāi)始自上而下逐層展開(kāi),呈現(xiàn)一個(gè)完整的正向的函數(shù)調(diào)用過(guò)程。

          分配采樣-樹(shù)狀視圖

          參考資料

          《JavaScript 高級(jí)程序設(shè)計(jì)(第4版)》

          Memory Management:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management

          Visualizing memory management in V8 Engine:https://deepu.tech/memory-management-in-v8/

          Trash talk: the Orinoco garbage collector:https://v8.dev/blog/trash-talk

          Fast properties in V8:https://v8.dev/blog/fast-properties

          Concurrent marking in V8:https://v8.dev/blog/concurrent-marking

          Chrome DevTools:https://developers.google.com/web/tools/chrome-devtools

          菜鳥(niǎo)小棧

          ??我是陳皮皮,一個(gè)還在不斷學(xué)習(xí)的游戲開(kāi)發(fā)者,一個(gè)熱愛(ài)分享的 Cocos Star Writer。

          ??這是我的個(gè)人公眾號(hào),專(zhuān)注但不僅限于游戲開(kāi)發(fā)和前端技術(shù)分享。

          ??每一篇原創(chuàng)都非常用心,你的關(guān)注就是我原創(chuàng)的動(dòng)力!

          Input and output.


          瀏覽 86
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  青青草原在线视频免费观看 | 欧美三级午夜理伦三级老人 | 99热0| 成人三级在线播放 | 人妻人人爽 |