溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

javascript中內(nèi)存指的是什么

發(fā)布時(shí)間:2022-09-20 14:05:19 來(lái)源:億速云 閱讀:169 作者:iii 欄目:web開(kāi)發(fā)

這篇文章主要介紹了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)存分析指南

內(nèi)存(memory)


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

相信大家都對(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 & 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)存的訪問(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ù)。

javascript中內(nèi)存指的是什么

? 棧內(nèi)存的大小

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

通常來(lái)說(shuō),每一條線程(Thread)都會(huì)有獨(dú)立的棧內(nèi)存空間,Windows 給每條線程分配的棧內(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)半毛錢關(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ù)。

javascript中內(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)程、線程與堆內(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)程的情況。

  • 函數(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é)束。

javascript中內(nèi)存指的是什么

上圖中的內(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)接著往下看。

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

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

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

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

? 原始類型(Primitive type)

原始類型又稱基本類型,包括 stringnumber、bigintboolean、undefined、nullsymbol(ES6 新增)。

原始類型的值被稱為原始值(Primitive value)。

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

? 引用類型(Reference type)

除了原始類型外,其余類型都屬于引用類型,包括 Object、Array、Function、DateRegExp、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)存。

javascript中內(nèi)存指的是什么

特別注意(Attention)

全局變量以及被閉包引用的變量(即使是原始類型)均儲(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ì)象引用。

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

原始值(Primitive values)

? 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)存里

javascript中內(nèi)存指的是什么

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

原始值的比較(Comparison)

當(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
對(duì)象引用(Object references)

? 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ì)象。

javascript中內(nèi)存指的是什么

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

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

所有引用類型的值實(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
對(duì)象的深拷貝(Deep copy)

當(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è)引用。

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

通常來(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)定性更高的代碼。

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

javascript中內(nèi)存指的是什么

上圖中的節(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)致程序的崩潰。

垃圾回收算法(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)烈建議你抽空專門去學(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ì)堵塞主線程,導(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á)的了。

javascript中內(nèi)存指的是什么

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

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

javascript中內(nèi)存指的是什么

引用計(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)稱為 ππ
// 而 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ì)象 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++ 編寫的高性能 JavaScript 引擎。

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

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

javascript中內(nèi)存指的是什么

這里我們只關(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í)間阻塞主線程了。

javascript中內(nèi)存指的是什么

當(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 啊~

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

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

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

全局變量(Global variable)

全局變量的訪問(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;
隱藏類(HiddenClass)

實(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)在讓我們專注于隱藏類。

javascript中內(nèi)存指的是什么

在上圖中可以很清楚地看到對(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)了。

閉包(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è)作品稱為:閉包炸彈
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)存情況呢?

“工欲善其事,必先利其器?!?/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 in Devtools(內(nèi)存面板)

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

javascript中內(nèi)存指的是什么

在這個(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ì)象。

Heap snapshot(堆快照)

javascript中內(nè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é)果。

javascript中內(nèi)存指的是什么

在堆快照結(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(類過(guò)濾器)中輸入構(gòu)造函數(shù)名稱來(lái)快速篩選對(duì)象。

javascript中內(nèi)存指的是什么

頁(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)類型(ArrayString、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();

javascript中內(nèi)存指的是什么

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

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

javascript中內(nèi)存指的是什么

? 注意到了嗎?

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

javascript中內(nèi)存指的是什么

Comparison(比較視圖)

只有同時(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。

javascript中內(nèi)存指的是什么

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

javascript中內(nèi)存指的是什么

Containment(包含視圖)

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

javascript中內(nèi)存指的是什么

包含視圖中有以下幾種全局對(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、DateRegExp、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

javascript中內(nèi)存指的是什么

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

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

javascript中內(nèi)存指的是什么

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

  • 紅色:Code(代碼)

  • 綠色:Strings(字符串)

  • 藍(lán)色:JS arrays(數(shù)組)

  • 橙色:Typed arrays(類型化數(shù)組)

  • 紫色:System objects(系統(tǒng)對(duì)象)

  • 白色:空閑內(nèi)存

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

javascript中內(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è)面。

javascript中內(nèi)存指的是什么

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

  • Summary:摘要視圖

  • Containment:包含視圖

  • Allocation:分配視圖

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

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

Summary(摘要視圖)

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

javascript中內(nèi)存指的是什么

? 時(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)左鍵即可取消選擇

javascript中內(nèi)存指的是什么

在時(shí)間軸中選擇要查看的時(shí)間段,即可得到該段時(shí)間的內(nèi)存分配詳情。

javascript中內(nèi)存指的是什么

Containment(包含視圖)

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

javascript中內(nèi)存指的是什么

Allocation(分配視圖)

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

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

javascript中內(nèi)存指的是什么

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

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

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

javascript中內(nèi)存指的是什么

Allocation sampling(分配采樣)

javascript中內(nèi)存指的是什么

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è)面。

javascript中內(nèi)存指的是什么

分配采樣結(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)存分配的不同階段(以內(nèi)存分配的大小為軸)。

javascript中內(nèi)存指的是什么

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

javascript中內(nèi)存指的是什么

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

javascript中內(nèi)存指的是什么

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

javascript中內(nèi)存指的是什么

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ò)程。

javascript中內(nèi)存指的是什么

視圖中的兩種 Size(大?。?/p>

  • 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ò)程。

javascript中內(nèi)存指的是什么

關(guān)于“javascript中內(nèi)存指的是什么”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對(duì)“javascript中內(nèi)存指的是什么”知識(shí)都有一定的了解,大家如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(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)容。

AI