前言
JavaScript 誕生于 1995 年传轰,最初被設(shè)計用于網(wǎng)頁內(nèi)的表單驗證剩盒。
這些年來 JavaScript 成長飛速,生態(tài)圈日益壯大,成為了最受程序員歡迎的開發(fā)語言之一辽聊。并且現(xiàn)在的 JavaScript 不再局限于網(wǎng)頁端纪挎,已經(jīng)擴展到了桌面端、移動端以及服務(wù)端跟匆。
隨著大前端時代的到來异袄,使用 JavaScript 的開發(fā)者越來越多,但是許多開發(fā)者都只停留在“會用”這個層面玛臂,而對于這門語言并沒有更多的了解烤蜕。
如果想要成為一名更好的 JavaScript 開發(fā)者,理解內(nèi)存是一個不可忽略的關(guān)鍵點迹冤。
?? 本文主要包含兩大部分:
- JavaScript 內(nèi)存詳解
- JavaScript 內(nèi)存分析指南
看完這篇文章后讽营,相信你會對 JavaScript 的內(nèi)存有比較全面的了解,并且能夠擁有獨自進行內(nèi)存分析的能力泡徙。
?? 話不多說橱鹏,我們開始吧!
文章篇幅較長堪藐,除去代碼也有 12000 字左右莉兰,需要一定的時間來閱讀,但是我保證你所花費的時間都是值得的礁竞。
正文
內(nèi)存(memory)
什么是內(nèi)存(What is memory)
相信大家都對內(nèi)存有一定的了解糖荒,我就不從盤古開天辟地開始講了,稍微提一下模捂。
首先寂嘉,任何應(yīng)用程序想要運行都離不開內(nèi)存。
另外枫绅,我們提到的內(nèi)存在不同的層面上有著不同的含義。
?? 硬件層面(Hardware)
在硬件層面上硼端,內(nèi)存指的是隨機存取存儲器并淋。
內(nèi)存是計算機重要組成部分,用來儲存應(yīng)用運行所需要的各種數(shù)據(jù)珍昨,CPU 能夠直接與內(nèi)存交換數(shù)據(jù)县耽,保證應(yīng)用能夠流暢運行。
一般來說镣典,在計算機的組成中主要有兩種隨機存取存儲器:高速緩存(Cache)和主存儲器(Main memory)兔毙。
高速緩存通常直接集成在 CPU 內(nèi)部,離我們比較遠兄春,所以更多時候我們提到的(硬件)內(nèi)存都是主存儲器澎剥。
?? 隨機存取存儲器(Random Access Memory,RAM)
隨機存取存儲器分為靜態(tài)隨機存取存儲器(Static Random Access Memory赶舆,SRAM)和動態(tài)隨機存取存儲器(Dynamic Random Access Memory叛拷,DRAM)兩大類封拧。
在速度上 SRAM 要遠快于 DRAM惫皱,而 SRAM 的速度僅次于 CPU 內(nèi)部的寄存器。
在現(xiàn)代計算機中倡蝙,高速緩存使用的是 SRAM,而主存儲器使用的是 DRAM绞佩。
?? 主存儲器(Main memory寺鸥,主存)
雖然高速緩存的速度很快,但是其存儲容量很小品山,小到幾 KB 最大也才幾十 MB胆建,根本不足以儲存應(yīng)用運行的數(shù)據(jù)。
我們需要一種存儲容量與速度適中的存儲部件谆奥,讓我們在保證性能的情況下眼坏,能夠同時運行幾十甚至上百個應(yīng)用,這也就是主存的作用酸些。
計算機中的主存其實就是我們平時說的內(nèi)存條(硬件)宰译。
硬件內(nèi)存不是我們今天的主題,所以就說這么多魄懂,想要深入了解的話可以根據(jù)上面提到關(guān)鍵詞進行搜索沿侈。
?? 軟件層面(Software)
在軟件層面上,內(nèi)存通常指的是操作系統(tǒng)從主存中劃分(抽象)出來的內(nèi)存空間市栗。
此時內(nèi)存又可以分為兩類:棧內(nèi)存和堆內(nèi)存缀拭。
接下來我將圍繞 JavaScript 這門語言來對內(nèi)存進行講解。
在后面的文章中所提到的內(nèi)存均指軟件層面上的內(nèi)存填帽。
棧與堆(Stack & Heap)
棧內(nèi)存(Stack memory)
?? 棧(Stack)
棧是一種常見的數(shù)據(jù)結(jié)構(gòu)蛛淋,棧只允許在結(jié)構(gòu)的一端操作數(shù)據(jù),所有數(shù)據(jù)都遵循后進先出(Last-In First-Out篡腌,LIFO)的原則褐荷。
現(xiàn)實生活中最貼切的的例子就是羽毛球桶,通常我們只通過球桶的一側(cè)來進行存取嘹悼,最先放進去的羽毛球只能最后被取出叛甫,而最后放進去的則會最先被取出。
棧內(nèi)存之所以叫做棧內(nèi)存杨伙,是因為棧內(nèi)存使用了棧的結(jié)構(gòu)其监。
棧內(nèi)存是一段連續(xù)的內(nèi)存空間,得益于棧結(jié)構(gòu)的簡單直接限匣,棧內(nèi)存的訪問和操作速度都非扯犊啵快。
棧內(nèi)存的容量較小,主要用于存放函數(shù)調(diào)用信息和變量等數(shù)據(jù)睛约,大量的內(nèi)存分配操作會導(dǎo)致棧溢出(Stack overflow)鼎俘。
棧內(nèi)存的數(shù)據(jù)儲存基本都是臨時性的,數(shù)據(jù)會在使用完之后立即被回收(如函數(shù)內(nèi)創(chuàng)建的局部變量在函數(shù)返回后就會被回收)辩涝。
簡單來說:棧內(nèi)存適合存放生命周期短贸伐、占用空間小且固定的數(shù)據(jù)。
?? 棧內(nèi)存的大小
棧內(nèi)存由操作系統(tǒng)直接管理怔揩,所以棧內(nèi)存的大小也由操作系統(tǒng)決定捉邢。
通常來說,每一條線程(Thread)都會有獨立的棧內(nèi)存空間商膊,Windows 給每條線程分配的棧內(nèi)存默認大小為 1MB伏伐。
堆內(nèi)存(Heap memory)
?? 堆(Heap)
堆也是一種常見的數(shù)據(jù)結(jié)構(gòu),但是不在本文討論范圍內(nèi)晕拆,就不多說了藐翎。
堆內(nèi)存雖然名字里有個“堆”字,但是它和數(shù)據(jù)結(jié)構(gòu)中的堆沒半毛錢關(guān)系实幕,就只是撞了名罷了吝镣。
堆內(nèi)存是一大片內(nèi)存空間,堆內(nèi)存的分配是動態(tài)且不連續(xù)的昆庇,程序可以按需申請堆內(nèi)存空間末贾,但是訪問速度要比棧內(nèi)存慢不少。
堆內(nèi)存里的數(shù)據(jù)可以長時間存在整吆,無用的數(shù)據(jù)需要程序主動去回收拱撵,如果大量無用數(shù)據(jù)占用內(nèi)存就會造成內(nèi)存泄露(Memory leak)。
簡單來說:堆內(nèi)存適合存放生命周期長表蝙,占用空間較大或占用空間不固定的數(shù)據(jù)拴测。
?? 堆內(nèi)存的上限
在 Node.js 中,堆內(nèi)存默認上限在 64 位系統(tǒng)中約為 1.4 GB府蛇,在 32 位系統(tǒng)中約為 0.7 GB昼扛。
而在 Chrome 瀏覽器中,每個標簽頁的內(nèi)存上限約為 4 GB(64 位系統(tǒng))和 1 GB(32 位系統(tǒng))欲诺。
?? 進程、線程與堆內(nèi)存
通常來說渺鹦,一個進程(Process)只會有一個堆內(nèi)存扰法,同一進程下的多個線程會共享同一個堆內(nèi)存。
在 Chrome 瀏覽器中毅厚,一般情況下每個標簽頁都有單獨的進程塞颁,不過在某些情況下也會出現(xiàn)多個標簽頁共享一個進程的情況。
函數(shù)調(diào)用(Function calling)
明白了棧內(nèi)存與堆內(nèi)存是什么后,現(xiàn)在讓我們看看當一個函數(shù)被調(diào)用時祠锣,棧內(nèi)存和堆內(nèi)存會發(fā)生什么變化酷窥。
當函數(shù)被調(diào)用時,會將函數(shù)推入棧內(nèi)存中伴网,生成一個棧幀(Stack frame)蓬推,棧幀可以理解為由函數(shù)的返回地址、參數(shù)和局部變量組成的一個塊澡腾;當函數(shù)調(diào)用另一個函數(shù)時沸伏,又會將另一個函數(shù)也推入棧內(nèi)存中,周而復(fù)始动分;直到最后一個函數(shù)返回毅糟,便從棧頂開始將棧內(nèi)存中的元素逐個彈出,直到棧內(nèi)存中不再有元素時則此次調(diào)用結(jié)束澜公。
上圖中的內(nèi)容經(jīng)過了簡化姆另,剝離了棧幀和各種指針的概念,主要展示函數(shù)調(diào)用以及內(nèi)存分配的大概過程坟乾。
在同一線程下(JavaScript 是單線程的)迹辐,所有被執(zhí)行的函數(shù)以及函數(shù)的參數(shù)和局部變量都會被推入到同一個棧內(nèi)存中,這也就是大量遞歸會導(dǎo)致棧溢出(Stack overflow)的原因糊渊。
關(guān)于圖中涉及到的函數(shù)內(nèi)部變量內(nèi)存分配的詳情請接著往下看右核。
儲存變量(Store variables)
當 JavaScript 程序運行時,在非全局作用域中產(chǎn)生的局部變量均儲存在棧內(nèi)存中渺绒。
但是贺喝,只有原始類型的變量是真正地把值儲存在棧內(nèi)存中。
而引用類型的變量只在棧內(nèi)存中儲存一個引用(reference)宗兼,這個引用指向堆內(nèi)存里的真正的值躏鱼。
?? 原始類型(Primitive type)
原始類型又稱基本類型,包括
string
殷绍、number
染苛、bigint
、boolean
主到、undefined
茶行、null
和symbol
(ES6 新增)。原始類型的值被稱為原始值(Primitive value)登钥。
補充:雖然
typeof null
返回的是'object'
畔师,但是null
真的不是對象,會出現(xiàn)這樣的結(jié)果其實是 JavaScript 的一個 Bug~
?? 引用類型(Reference type)
除了原始類型外牧牢,其余類型都屬于引用類型看锉,包括
Object
姿锭、Array
、Function
伯铣、Date
呻此、RegExp
、String
腔寡、Number
焚鲜、Boolean
等等...實際上
Object
是最基本的引用類型,其他引用類型均繼承自Object
蹬蚁。也就是說恃泪,所有引用類型的值實際上都是對象。引用類型的值被稱為引用值(Reference value)犀斋。
?? 簡單來說
在多數(shù)情況下贝乎,原始類型的數(shù)據(jù)儲存在棧內(nèi)存,而引用類型的數(shù)據(jù)(對象)則儲存在堆內(nèi)存叽粹。
特別注意(Attention)
全局變量以及被閉包引用的變量(即使是原始類型)均儲存在堆內(nèi)存中览效。
?? 全局變量(Global variables)
在全局作用域下創(chuàng)建的所有變量都會成為全局對象(如 window
對象)的屬性,也就是全局變量虫几。
而全局對象儲存在堆內(nèi)存中锤灿,所以全局變量必然也會儲存在堆內(nèi)存中。
不要問我為什么全局對象儲存在堆內(nèi)存中辆脸,一會我翻臉了暗!!
?? 閉包(Closures)
在函數(shù)(局部作用域)內(nèi)創(chuàng)建的變量均為局部變量啡氢。
當一個局部變量被當前函數(shù)之外的其他函數(shù)所引用(也就是發(fā)生了逃逸)状囱,此時這個局部變量就不能隨著當前函數(shù)的返回而被回收,那么這個變量就必須儲存在堆內(nèi)存中倘是。
而這里的“其他函數(shù)”就是我們說的閉包亭枷,就如下面這個例子:
function getCounter() {
let count = 0;
function counter() {
return ++count;
}
return counter;
}
// closure 是一個閉包函數(shù)
// 變量 count 發(fā)生了逃逸
let closure = getCounter();
closure(); // 1
closure(); // 2
closure(); // 3
閉包是一個非常重要且常用的概念,許多編程語言里都有閉包這個概念搀崭。這里就不詳細介紹了叨粘,貼一篇阮一峰大佬的文章。
學(xué)習(xí) JavaScript 閉包:http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
?? 逃逸分析(Escape Analysis)
實際上瘤睹,JavaScript 引擎會通過逃逸分析來決定變量是要儲存在棧內(nèi)存還是堆內(nèi)存中升敲。
簡單來說,逃逸分析是一種用來分析變量的作用域的機制轰传。
不可變與可變(Immutable and Mutable)
棧內(nèi)存中會儲存兩種變量數(shù)據(jù):原始值和對象引用驴党。
不僅類型不同,它們在棧內(nèi)存中的具體表現(xiàn)也不太一樣绸吸。
原始值(Primitive values)
?? Primitive values are immutable!
前面有說到:原始類型的數(shù)據(jù)(原始值)直接儲存在棧內(nèi)存中鼻弧。
⑴ 當我們定義一個原始類型變量的時候,JavaScript 會在棧內(nèi)存中激活一塊內(nèi)存來儲存變量的值(原始值)锦茁。
⑵ 當我們更改原始類型變量的值時攘轩,實際上會再激活一塊新的內(nèi)存來儲存新的值,并將變量指向新的內(nèi)存空間码俩,而不是改變原來那塊內(nèi)存里的值度帮。
⑶ 當我們將一個原始類型變量賦值給另一個新的變量(也就是復(fù)制變量)時,也是會再激活一塊新的內(nèi)存稿存,并將源變量內(nèi)存里的值復(fù)制一份到新的內(nèi)存里笨篷。
?? 總之就是:棧內(nèi)存中的原始值一旦確定就不能被更改(不可變的)。
原始值的比較(Comparison)
當我們比較原始類型的變量時瓣履,會直接比較棧內(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)
?? Object references are mutable!
前面也有說到:引用類型的變量在棧內(nèi)存中儲存的只是一個指向堆內(nèi)存的引用袖迎。
⑴ 當我們定義一個引用類型的變量時冕臭,JavaScript 會先在堆內(nèi)存中找到一塊合適的地方來儲存對象,并激活一塊棧內(nèi)存來儲存對象的引用(堆內(nèi)存地址)燕锥,最后將變量指向這塊棧內(nèi)存辜贵。
?? 所以當我們通過變量訪問對象時,實際的訪問過程應(yīng)該是:
變量 -> 棧內(nèi)存中的引用 -> 堆內(nèi)存中的值
⑵ 當我們把引用類型變量賦值給另一個變量時归形,會將源變量指向的棧內(nèi)存中的對象引用復(fù)制到新變量的棧內(nèi)存中托慨,所以實際上只是復(fù)制了個對象引用,并沒有在堆內(nèi)存中生成一份新的對象暇榴。
⑶ 而當我們給引用類型變量分配為一個新的對象時厚棵,則會直接修改變量指向的棧內(nèi)存中的引用,新的引用指向堆內(nèi)存中新的對象跺撼。
?? 總之就是:棧內(nèi)存中的對象引用是可以被更改的(可變的)窟感。
對象的比較(Comparison)
所有引用類型的值實際上都是對象。
當我們比較引用類型的變量時歉井,實際上是在比較棧內(nèi)存中的引用柿祈,只有引用相同時變量才相等。
即使是看起來完全一樣的兩個引用類型變量哩至,只要他們的引用的不是同一個值躏嚎,那么他們就是不一樣。
// 兩個變量指向的是兩個不同的引用
// 雖然這兩個對象看起來完全一樣
// 但它們確確實實是不同的對象實例
let a = { name: 'pp' }
let b = { name: 'pp' }
console.log(a === b); // false
// 直接賦值的方式復(fù)制的是對象的引用
let c = a;
console.log(a === c); // true
對象的深拷貝(Deep copy)
當我們搞明白引用類型變量在內(nèi)存中的表現(xiàn)時菩貌,就能清楚地理解為什么淺拷貝對象是不可靠的卢佣。
在淺拷貝中,簡單的賦值只會復(fù)制對象的引用箭阶,實際上新變量和源變量引用的都是同一個對象虚茶,修改時也是修改的同一個對象戈鲁,這顯然不是我們想要的。
想要真正的復(fù)制一個對象嘹叫,就必須新建一個對象婆殿,將源對象的屬性復(fù)制過去;如果遇到引用類型的屬性罩扇,那就再新建一個對象婆芦,繼續(xù)復(fù)制...
此時我們就需要借助遞歸來實現(xiàn)多層次對象的復(fù)制,這也就是我們說的深拷貝喂饥。
對于任何引用類型的變量消约,都應(yīng)該使用深拷貝來復(fù)制,除非你很確定你的目的就是復(fù)制一個引用员帮。
內(nèi)存生命周期(Memory life cycle)
通常來說或粮,所有應(yīng)用程序的內(nèi)存生命周期都是基本一致的:
分配 -> 使用 -> 釋放
當我們使用高級語言編寫程序時,往往不會涉及到內(nèi)存的分配與釋放操作集侯,因為分配與釋放均已經(jīng)在底層語言中實現(xiàn)了被啼。
對于 JavaScript 程序來說,內(nèi)存的分配與釋放是由 JavaScript 引擎自動完成的(目前的 JavaScript 引擎基本都是使用 C++ 或 C 編寫的)棠枉。
但是這不意味著我們就不需要在乎內(nèi)存管理浓体,了解內(nèi)存的更多細節(jié)可以幫助我們寫出性能更好,穩(wěn)定性更高的代碼辈讶。
垃圾回收(Garbage collection)
垃圾回收即我們常說的 GC(Garbage collection)命浴,也就是清除內(nèi)存中不再需要的數(shù)據(jù),釋放內(nèi)存空間贱除。
由于棧內(nèi)存由操作系統(tǒng)直接管理生闲,所以當我們提到 GC 時指的都是堆內(nèi)存的垃圾回收。
基本上現(xiàn)在的瀏覽器的 JavaScript 引擎(如 V8 和 SpiderMonkey)都實現(xiàn)了垃圾回收機制月幌,引擎中的垃圾回收器(Garbage collector)會定期進行垃圾回收碍讯。
?? 緊急補課
在我們繼續(xù)之前,必須先了解“可達性”和“內(nèi)存泄露”這兩個概念:
?? 可達性(Reachability)
在 JavaScript 中扯躺,可達性指的是一個變量是否能夠直接或間接通過全局對象訪問到捉兴,如果可以那么該變量就是可達的(Reachable),否則就是不可達的(Unreachable)录语。
上圖中的節(jié)點 9 和節(jié)點 10 均無法通過節(jié)點 1(根節(jié)點)直接或間接訪問倍啥,所以它們都是不可達的,可以被安全地回收澎埠。
?? 內(nèi)存泄漏(Memory leak)
內(nèi)存泄露指的是程序運行時由于某種原因未能釋放那些不再使用的內(nèi)存虽缕,造成內(nèi)存空間的浪費。
輕微的內(nèi)存泄漏或許不太會對程序造成什么影響蒲稳,但是一旦泄露變嚴重氮趋,就會開始影響程序的性能伍派,甚至導(dǎo)致程序的崩潰。
垃圾回收算法(Algorithms)
垃圾回收的基本思路很簡單:確定哪個變量不會再使用剩胁,然后釋放它占用的內(nèi)存拙已。
實際上,在回收過程中想要確定一個變量是否還有用并不簡單摧冀。
直到現(xiàn)在也還沒有一個真正完美的垃圾回收算法,接下來介紹 3 種最廣為人知的垃圾回收算法系宫。
標記-清除(Mark-and-Sweep)
標記清除算法是目前最常用的垃圾收集算法之一索昂。
從該算法的名字上就可以看出,算法的關(guān)鍵就是標記與清除扩借。
標記指的是標記變量的狀態(tài)的過程椒惨,標記變量的具體方法有很多種,但是基本理念是相似的潮罪。
對于標記算法我們不需要知道所有細節(jié)康谆,只需明白標記的基本原理即可。
需要注意的是嫉到,這個算法的效率不算高沃暗,同時會引起內(nèi)存碎片化的問題。
?? 舉個栗子
當一個變量進入執(zhí)行上下文時何恶,它就會被標記為“處于上下文中”孽锥;而當變量離開執(zhí)行上下文時,則會被標記為“已離開上下文”细层。
?? 執(zhí)行上下文(Execution context)
執(zhí)行上下文是 JavaScript 中非常重要的概念惜辑,簡單來說的是代碼執(zhí)行的環(huán)境。
如果你現(xiàn)在對于執(zhí)行上下文還不是很了解疫赎,我強烈建議你抽空專門去學(xué)習(xí)下J⒊拧!捧搞!
垃圾回收器將定期掃描內(nèi)存中的所有變量抵卫,將處于上下文中以及被處于上下文中的變量引用的變量的標記去除,將其余變量標記為“待刪除”实牡。
隨后陌僵,垃圾回收器會清除所有帶有“待刪除”標記的變量,并釋放它們所占用的內(nèi)存创坞。
標記-整理(Mark-Compact)
準確來說炉峰,Compact 應(yīng)譯為緊湊澄阳、壓縮,但是在這里我覺得用“整理”更為貼切盆佣。
標記整理算法也是常用的垃圾收集算法之一。
使用標記整理算法可以解決內(nèi)存碎片化的問題(通過整理)延都,提高內(nèi)存空間的可用性。
但是,該算法的標記階段比較耗時闰渔,可能會堵塞主線程,導(dǎo)致程序長時間處于無響應(yīng)狀態(tài)铐望。
雖然算法的名字上只有標記和整理冈涧,但這個算法通常有 3 個階段,即標記正蛙、整理與清除督弓。
?? 以 V8 的標記整理算法為例
① 首先,在標記階段乒验,垃圾回收器會從全局對象(根)開始愚隧,一層一層往下查詢,直到標記完所有活躍的對象锻全,那么剩下的未被標記的對象就是不可達的了狂塘。
② 然后是整理階段(碎片整理),垃圾回收器會將活躍的(被標記了的)對象往內(nèi)存空間的一端移動鳄厌,這個過程可能會改變內(nèi)存中的對象的內(nèi)存地址荞胡。
③ 最后來到清除階段,垃圾回收器會將邊界后面(也就是最后一個活躍的對象后面)的對象清除了嚎,并釋放它們占用的內(nèi)存空間硝训。
引用計數(shù)(Reference counting)
引用計數(shù)算法是基于“引用計數(shù)”實現(xiàn)的垃圾回收算法,這是最初級但已經(jīng)被棄用的垃圾回收算法新思。
引用計數(shù)算法需要 JavaScript 引擎在程序運行時記錄每個變量被引用的次數(shù)窖梁,隨后根據(jù)引用的次數(shù)來判斷變量是否能夠被回收。
雖然垃圾回收已不再使用引用計數(shù)算法夹囚,但是引用計數(shù)技術(shù)仍非常有用纵刘!
?? 舉個栗子
注意:垃圾回收不是即使生效的!但是在下面的例子中我們將假設(shè)回收是立即生效的荸哟,這樣會更好理解~
// 下面我將 name 屬性為 ππ 的對象簡稱為 ππ
// 而 name 屬性為 pp 的對象則簡稱為 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)在 ππ 沒有任何引用了,可以被回收了
// 在 ππ 被回收后劣砍,pp 的引用也會相應(yīng)減少
// ππ 的引用:0惧蛹,pp 的引用:1
b = null;
// 現(xiàn)在 pp 也可以被回收了
// ππ 的引用:0,pp 的引用:0
x = null;
// 哦豁,這下全完了香嗓!
?? 循環(huán)引用(Circular references)
引用計數(shù)算法看似很美好迅腔,但是它有一個致命的缺點,就是無法處理循環(huán)引用的情況靠娱。
在下方的例子中沧烈,當 foo()
函數(shù)執(zhí)行完畢之后,對象 a
與 b
都已經(jīng)離開了作用域像云,理論上它們都應(yīng)該能夠被回收才對锌雀。
但是由于它們互相引用了對方,所以垃圾回收器就認為他們都還在被引用著迅诬,導(dǎo)致它們哥倆永遠都不會被回收汤锨,這就造成了內(nèi)存泄露。
function foo() {
let a = { o: null };
let b = { o: null };
a.o = b;
b.o = a;
}
foo();
// 即使 foo 函數(shù)已經(jīng)執(zhí)行完畢
// 對象 a 和 b 均已離開函數(shù)作用域
// 但是 a 和 b 還在互相引用
// 那么它們這輩子都不會被回收了
// Oops百框!內(nèi)存泄露了!
V8 中的垃圾回收(GC in V8)
8?? V8
V8 是一個由 Google 開源的用 C++ 編寫的高性能 JavaScript 引擎牍汹。
V8 是目前最流行的 JavaScript 引擎之一铐维,我們熟知的 Chrome 瀏覽器和 Node.js 等軟件都在使用 V8。
在 V8 的內(nèi)存管理機制中慎菲,把堆內(nèi)存(Heap memory)劃分成了多個區(qū)域嫁蛇。
這里我們只關(guān)注這兩個區(qū)域:
- New Space(新空間):又稱 Young generation(新世代),用于儲存新生成的對象露该,由 Minor GC 進行管理睬棚。
- Old Space(舊空間):又稱 Old generation(舊世代),用于儲存那些在兩次 GC 后仍然存活的對象解幼,由 Major GC 進行管理抑党。
也就是說,只要 New Space 里的對象熬過了兩次 GC撵摆,就會被轉(zhuǎn)移到 Old Space底靠,變成老油條。
?? 雙管齊下
V8 內(nèi)部實現(xiàn)了兩個垃圾回收器:
- Minor GC(副 GC):它還有個名字叫做 Scavenger(清道夫)特铝,具體使用的是 Cheney's Algorithm(Cheney 算法)暑中。
- Major GC(主 GC):使用的是文章前面提到的 Mark-Compact Algorithm(標記-整理算法)。
儲存在 New Space 里的新生對象大多都只是臨時使用的鲫剿,而且 New Space 的容量比較小鳄逾,為了保持內(nèi)存的可用率,Minor GC 會頻繁地運行灵莲。
而 Old Space 里的對象存活時間都比較長雕凹,所以 Major GC 沒那么勤快,這一定程度地降低了頻繁 GC 帶來的性能損耗。
?? 加點魔法
我們在上方的“標記整理算法”中有提到這個算法的標記過程非常耗時请琳,所以很容易導(dǎo)致應(yīng)用長時間無響應(yīng)粱挡。
為了提升用戶體驗,V8 還實現(xiàn)了一個名為增量標記(Incremental marking)的特性俄精。
增量標記的要點就是把標記工作分成多個小段询筏,夾雜在主線程(Main thread)的 JavaScript 邏輯中,這樣就不會長時間阻塞主線程了竖慧。
當然增量標記也有代價的嫌套,在增量標記過程中所有對象的變化都需要通知垃圾回收器,好讓垃圾回收器能夠正確地標記那些對象圾旨,這里的“通知”也是需要成本的踱讨。
另外 V8 中還有使用工作線程(Worker thread)實現(xiàn)的平行標記(Parallel marking)和并行標記(Concurrent marking),這里我就不再細說了~
?? 總結(jié)一下
為了提升性能和用戶體驗砍的,V8 內(nèi)部做了非常非常多的“騷操作”痹筛,本文提到的都只是冰山一角,但足以讓我五體投地佩服連連廓鞠!
總之就是非常 Amazing 啊~
內(nèi)存管理(Memory management)
或者說是:內(nèi)存優(yōu)化(Memory optimization)帚稠?
雖然我們寫代碼的時候一般不會直接接觸內(nèi)存管理,但是有一些注意事項可以讓我們避免引起內(nèi)存問題床佳,甚至提升代碼的性能滋早。
全局變量(Global variable)
全局變量的訪問速度遠不及局部變量,應(yīng)盡量避免定義非必要的全局變量砌们。
在我們實際的項目開發(fā)中杆麸,難免會需要去定義一些全局變量,但是我們必須謹慎使用全局變量浪感。
因為全局變量永遠都是可達的昔头,所以全局變量永遠不會被回收。
?? 還記得“可達性”這個概念嗎影兽?
因為全局變量直接掛載在全局對象上减细,也就是說全局變量永遠都可以通過全局對象直接訪問。
所以全局變量永遠都是可達的赢笨,而可達的變量永遠都不會被回收未蝌。
?? 應(yīng)該怎么做?
當一個全局變量不再需要用到時茧妒,記得解除其引用(置空)萧吠,好讓垃圾回收器可以釋放這部分內(nèi)存。
// 全局變量不會被回收
window.me = {
name: '吳彥祖',
speak: function() {
console.log(`我是${this.name}`);
}
};
window.me.speak();
// 解除引用后才可以被回收
window.me = null;
隱藏類(HiddenClass)
實際上的隱藏類遠比本文所提到的復(fù)雜桐筏,但是今天的主角不是它纸型,所以我們點到為止。
在 V8 內(nèi)部有一個叫做“隱藏類”的機制,主要用于提升對象(Object)的性能狰腌。
V8 里的每一個 JS 對象(JS Objects)都會關(guān)聯(lián)一個隱藏類除破,隱藏類里面儲存了對象的形狀(特征)和屬性名稱到屬性的映射等信息。
隱藏類內(nèi)記錄了每個屬性的內(nèi)存偏移(Memory offset)琼腔,后續(xù)訪問屬性的時候就可以快速定位到對應(yīng)屬性的內(nèi)存位置瑰枫,從而提升對象屬性的訪問速度。
在我們創(chuàng)建對象時丹莲,擁有完全相同的特征(相同屬性且相同順序)的對象可以共享同一個隱藏類光坝。
?? 再想象一下
我們可以把隱藏類想象成工業(yè)生產(chǎn)中使用的模具,有了模具之后甥材,產(chǎn)品的生產(chǎn)效率得到了很大的提升盯另。
但是如果我們更改了產(chǎn)品的形狀,那么原來的模具就不能用了洲赵,又需要制作新的模具才行鸳惯。
?? 舉個栗子
在 Chrome 瀏覽器 Devtools 的 Console 面板中執(zhí)行以下代碼:
// 對象 A
let objectA = {
id: 'A',
name: '吳彥祖'
};
// 對象 B
let objectB = {
id: 'B',
name: '彭于晏'
};
// 對象 C
let objectC = {
id: 'C',
name: '劉德華',
gender: '男'
};
// 對象 A 和 B 擁有完全相同的特征
// 所以它們可以使用同一個隱藏類
// good!
隨后在 Memory 面板打一個堆快照,通過堆快照中的 Comparison 視圖可以快速找到上面創(chuàng)建的 3 個對象:
注:關(guān)于如何查看內(nèi)存中的對象將會在文章的第二大部分中進行講解叠萍,現(xiàn)在讓我們專注于隱藏類芝发。
在上圖中可以很清楚地看到對象 A 和 B 確實使用了同一個隱藏類。
而對象 C 因為多了一個 gender
屬性俭令,所以不能和前面兩個對象共享隱藏類。
?? 動態(tài)增刪對象屬性
一般情況下部宿,當我們動態(tài)修改對象的特征(增刪屬性)時抄腔,V8 會為該對象分配一個能用的隱藏類或者創(chuàng)建一個新的隱藏類(新的分支)。
例如動態(tài)地給對象增加一個新的屬性:
注:這種操作被稱為“先創(chuàng)建再補充(ready-fire-aim)”理张。
// 增加 gender 屬性
objectB.gender = '男';
// 對象 B 的特征發(fā)生了變化
// 多了一個原本沒有的 gender 屬性
// 導(dǎo)致對象 B 不能再與 A 共享隱藏類
// bad!
動態(tài)刪除(delete
)對象的屬性也會導(dǎo)致同樣的結(jié)果:
// 刪除 name 屬性
delete objectB.name;
// A:我們不一樣赫蛇!
// bad!
不過,添加數(shù)組索引屬性(Array-indexed properties)并不會有影響:
其實就是用整數(shù)作為屬性名雾叭,此時 V8 會另外處理悟耘。
// 增加 1 屬性
objectB[1] = '數(shù)字組引屬性';
// 不影響共享隱藏類
// so far so good!
?? 那問題來了
說了這么多,隱藏類看起來確實可以提升性能织狐,那它和內(nèi)存又有什么關(guān)系呢暂幼?
實際上,隱藏類也需要占用內(nèi)存空間移迫,這其實就是一種用空間換時間的機制旺嬉。
如果由于動態(tài)增刪對象屬性而創(chuàng)建了大量隱藏類和分支,結(jié)果就是會浪費不少內(nèi)存空間厨埋。
?? 舉個栗子
創(chuàng)建 1000 個擁有相同屬性的對象邪媳,內(nèi)存中只會多出 1 個隱藏類。
而創(chuàng)建 1000 個屬性信息完全不同的對象,內(nèi)存中就會多出 1000 個隱藏類雨效。
?? 應(yīng)該怎么做迅涮?
所以,我們要盡量避免動態(tài)增刪對象屬性操作徽龟,應(yīng)該在構(gòu)造函數(shù)內(nèi)就一次性聲明所有需要用到的屬性叮姑。
如果確實不再需要某個屬性,我們可以將屬性的值設(shè)為 null
顿肺,如下:
// 將 age 屬性置空
objectB.age = null;
// still good!
另外戏溺,相同名稱的屬性盡量按照相同的順序來聲明,可以盡可能地讓更多對象共享相同的隱藏類屠尊。
即使遇到不能共享隱藏類的情況旷祸,也至少可以減少隱藏類分支的產(chǎn)生。
其實動態(tài)增刪對象屬性所引起的性能問題更為關(guān)鍵讼昆,但因本文篇幅有限托享,就不再展開了。
閉包(Closure)
前面有提到:被閉包引用的變量儲存在堆內(nèi)存中浸赫。
這里我們再重點關(guān)注一下閉包中的內(nèi)存問題闰围,還是前面的例子:
function getCounter() {
let count = 0;
function counter() {
return ++count;
}
return counter;
}
// closure 是一個閉包函數(shù)
let closure = getCounter();
closure(); // 1
closure(); // 2
closure(); // 3
現(xiàn)在只要我們一直持有變量(函數(shù)) closure
,那么變量 count
就不會被釋放既峡。
或許你還沒有發(fā)現(xiàn)風(fēng)險所在羡榴,不如讓我們試想變量 count
不是一個數(shù)字,而是一個巨大的數(shù)組运敢,一但這樣的閉包多了校仑,那對于內(nèi)存來說就是災(zāi)難。
// 我將這個作品稱為:閉包炸彈
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)該怎么做传惠?
所以迄沫,我們必須避免濫用閉包,并且謹慎使用閉包卦方!
當不再需要時記得解除閉包函數(shù)的引用羊瘩,讓閉包函數(shù)以及引用的變量能夠被回收。
closure = null;
// 變量 count 終于得救了
如何分析內(nèi)存(Analyze)
說了這么多盼砍,那我們應(yīng)該如何查看并分析程序運行時的內(nèi)存情況呢尘吗?
“工欲善其事,必先利其器浇坐∫∮瑁”
對于 Web 前端項目來說,分析內(nèi)存的最佳工具非 Memory 莫屬吗跋!
這里的 Memory 指的是 DevTools 中的一個工具侧戴,為了避免混淆宁昭,下面我會用“Memory 面板”或”內(nèi)存面板“代稱。
?? DevTools(開發(fā)者工具)
DevTools 是瀏覽器里內(nèi)置的一套用于 Web 開發(fā)和調(diào)試的工具酗宋。
使用 Chromuim 內(nèi)核的瀏覽器都帶有 DevTools积仗,個人推薦使用 Chrome 或者 Edge(新)。
Memory in Devtools(內(nèi)存面板)
在我們切換到 Memory 面板后蜕猫,會看到以下界面(注意標注):
在這個面板中寂曹,我們可以通過 3 種方式來記錄內(nèi)存情況:
- Heap snapshot:堆快照
- Allocation instrumentation on timeline:內(nèi)存分配時間軸
- Allocation sampling:內(nèi)存分配采樣
小貼士:點擊面板左上角的 Collect garbage 按鈕(垃圾桶圖標)可以主動觸發(fā)垃圾回收。
?? 在正式開始分析內(nèi)存之前回右,讓我們先學(xué)習(xí)幾個重要的概念:
?? Shallow Size(淺層大新≡病)
淺層大小指的是當前對象自身占用的內(nèi)存大小。
淺層大小不包含自身引用的對象翔烁。
?? Retained Size(保留大忻煅酢)
保留大小指的是當前對象被 GC 回收后總共能夠釋放的內(nèi)存大小。
換句話說蹬屹,也就是當前對象自身大小加上對象直接或間接引用的其他對象的大小總和侣背。
需要注意的是,保留大小不包含那些除了被當前對象引用之外還被全局對象直接或間接引用的對象慨默。
Heap snapshot(堆快照)
堆快照可以記錄頁面當前時刻的 JS 對象以及 DOM 節(jié)點的內(nèi)存分配情況贩耐。
?? 如何開始
點擊頁面底部的 Take snapshot 按鈕或者左上角的 ? 按鈕即可打一個堆快照,片刻之后就會自動展示結(jié)果厦取。
在堆快照結(jié)果頁面中潮太,我們可以使用 4 種不同的視圖來觀察內(nèi)存情況:
- Summary:摘要視圖
- Comparison:比較視圖
- Containment:包含視圖
- Statistics:統(tǒng)計視圖
默認顯示 Summary 視圖。
Summary(摘要視圖)
摘要視圖根據(jù) Constructor(構(gòu)造函數(shù))來將對象進行分組虾攻,我們可以在 Class filter(類過濾器)中輸入構(gòu)造函數(shù)名稱來快速篩選對象铡买。
頁面中的幾個關(guān)鍵詞:
- Constructor:構(gòu)造函數(shù)。
- Distance:(根)距離台谢,對象與 GC 根之間的最短距離寻狂。
- Shallow Size:淺層大小岁经,單位:Bytes(字節(jié))朋沮。
- Retained Size:保留大小,單位:Bytes(字節(jié))缀壤。
- Retainers:持有者樊拓,也就是直接引用目標對象的變量。
?? Retainers(持有者)
Retainers 欄在舊版的 Devtools 里叫做 Object's retaining tree(對象保留樹)塘慕。
Retainers 下的對象也展開為樹形結(jié)構(gòu)筋夏,方便我們進行引用溯源。
在視圖中的構(gòu)造函數(shù)列表中图呢,有一些用“()”包裹的條目:
- (compiled code):已編譯的代碼条篷。
- (closure):閉包函數(shù)骗随。
-
(array, string, number, symbol, regexp):對應(yīng)類型(
Array
、String
赴叹、Number
鸿染、Symbol
、RegExp
)的數(shù)據(jù)乞巧。 -
(concatenated string):使用
concat()
函數(shù)拼接而成的字符串涨椒。 -
(sliced string):使用
slice()
、substring()
等函數(shù)進行邊緣切割的字符串绽媒。 - (system):系統(tǒng)(引擎)產(chǎn)生的對象蚕冬,如 V8 創(chuàng)建的 HiddenClasses(隱藏類)和 DescriptorArrays(描述符數(shù)組)等數(shù)據(jù)。
?? DescriptorArrays(描述符數(shù)組)
描述符數(shù)組主要包含對象的屬性名信息是辕,是隱藏類的重要組成部分囤热。
不過描述符數(shù)組內(nèi)不會包含整數(shù)索引屬性。
而其余沒有用“()”包裹的則為全局屬性和 GC 根免糕。
另外赢乓,每個對象后面都會有一串“@”開頭的數(shù)字,這是對象在內(nèi)存中的唯一 ID石窑。
小貼士:按下快捷鍵 Ctrl/Command + F 展示搜索欄牌芋,輸入名稱或 ID 即可快速查找目標對象。
?? 實踐一下:實例化一個對象
① 切換到 Console 面板松逊,執(zhí)行以下代碼來實例化一個對象:
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 面板躺屁,打一個堆快照,在 Class filter 中輸入“TestClass”:
可以看到內(nèi)存中有一個 TestClass
的實例经宏,該實例的淺層大小為 80 字節(jié)犀暑,保留大小為 876 字節(jié)。
?? 注意到了嗎烁兰?
堆快照中的
TestClass
實例的屬性中少了一個名為number
屬性耐亏,這是因為堆快照不會捕捉數(shù)字屬性。
?? 實踐一下:創(chuàng)建一個字符串
① 切換到 Console 面板沪斟,執(zhí)行以下代碼來創(chuàng)建一個字符串:
// 這是一個全局變量
let testString = '我是吳彥祖';
② 回到 Memory 面板广辰,打一個堆快照,打開搜索欄(Ctrl/Command + F)并輸入“我是吳彥祖”:
Comparison(比較視圖)
只有同時存在 2 個或以上的堆快照時才會出現(xiàn) Comparison 選項主之。
比較視圖用于展示兩個堆快照之間的差異择吊。
使用比較視圖可以讓我們快速得知在執(zhí)行某個操作后的內(nèi)存變化情況(如新增或減少對象)。
通過多個快照的對比還可以讓我們快速判斷并定位內(nèi)存泄漏槽奕。
文章前面提到隱藏類的時候几睛,就是使用了比較視圖來快速查找新創(chuàng)建的對象。
?? 實踐一下
① 新建一個無痕(匿名)標簽頁并切換到 Memory 面板粤攒,打一個堆快照 Snapshot 1所森。
?? 為什么是無痕標簽頁囱持?
普通標簽頁會受到瀏覽器擴展或者其他腳本影響,內(nèi)存占用不穩(wěn)定焕济。
使用無痕窗口的標簽頁可以保證頁面的內(nèi)存相對純凈且穩(wěn)定洪唐,有利于我們進行對比。
另外吼蚁,建議打開窗口一段之間之后再開始測試凭需,這樣內(nèi)存會比較穩(wěn)定(控制變量)。
② 切換到 Console 面板肝匆,執(zhí)行以下代碼來實例化一個 Foo
對象:
function Foo() {
this.name = 'pp';
this.age = 18;
}
let foo = new Foo();
③ 回到 Memory 面板粒蜈,再打一個堆快照 Snapshot 2,切換到 Comparison 視圖旗国,選擇 Snapshot 1 作為 Base snapshot(基本快照)枯怖,在 Class filter 中輸入“Foo”:
可以看到內(nèi)存中新增了一個 Foo
對象實例,分配了 52 字節(jié)內(nèi)存空間能曾,該實例的引用持有者為變量 foo
度硝。
④ 再次切換到 Console 面板,執(zhí)行以下代碼來解除變量 foo
的引用:
// 解除對象的引用
foo = null;
⑤ 再回到 Memory 面板寿冕,打一個堆快照 Snapshot 3蕊程,選擇 Snapshot 2 作為 Base snapshot,在 Class filter 中輸入“Foo”:
內(nèi)存中的 Foo
對象實例已經(jīng)被刪除驼唱,釋放了 52 字節(jié)的內(nèi)存空間藻茂。
Containment(包含視圖)
包含視圖就是程序?qū)ο蠼Y(jié)構(gòu)的“鳥瞰圖(Bird's eye view)”,允許我們通過全局對象出發(fā)玫恳,一層一層往下探索诵叁,從而了解內(nèi)存的詳細情況鳞绕。
包含視圖中有以下幾種全局對象:
GC roots(GC 根)
GC roots 就是 JavaScript 虛擬機的垃圾回收中實際使用的根節(jié)點封断。
GC 根可以由 Built-in object maps(內(nèi)置對象映射)而昨、Symbol tables(符號表)、VM thread stacks(VM 線程堆棧)惭婿、Compilation caches(編譯緩存)不恭、Handle scopes(句柄作用域)和 Global handles(全局句柄)等組成。
DOMWindow objects(DOMWindow 對象)
DOMWindow objects 指的是由宿主環(huán)境(瀏覽器)提供的頂級對象审孽,也就是 JavaScript 代碼中的全局對象 window
县袱,每個標簽頁都有自己的 window
對象(即使是同一窗口)浑娜。
Native objects(原生對象)
Native objects 指的是那些基于 ECMAScript 標準實現(xiàn)的內(nèi)置對象佑力,包括 Object
、Function
筋遭、Array
打颤、String
暴拄、Boolean
、Number
编饺、Date
乖篷、RegExp
、Math
等對象透且。
?? 實踐一下
① 切換到 Console 面板撕蔼,執(zhí)行以下代碼來創(chuàng)建一個構(gòu)造函數(shù) $ABC
:
構(gòu)造函數(shù)命名前面加個 $ 是因為這樣排序的時候可以排在前面,方便找秽誊。
function $ABC() {
this.name = 'pp';
}
② 切換到 Memory 面板鲸沮,打一個堆快照,切換為 Containment 視圖:
在當前標簽頁的全局對象下就可以找到我們剛剛創(chuàng)建的構(gòu)造函數(shù) $ABC
锅论。
Statistics(統(tǒng)計視圖)
統(tǒng)計視圖可以很直觀地展示內(nèi)存整體分配情況讼溺。
在該視圖里的空心餅圖中共有 6 種顏色,各含義分別為:
- 紅色:Code(代碼)
- 綠色:Strings(字符串)
- 藍色:JS arrays(數(shù)組)
- 橙色:Typed arrays(類型化數(shù)組)
- 紫色:System objects(系統(tǒng)對象)
- 白色:空閑內(nèi)存
Allocation instrumentation on timeline(分配時間軸)
在一段時間內(nèi)持續(xù)地記錄內(nèi)存分配(約每 50 毫秒打一張堆快照)最易,記錄完成后可以選擇查看任意時間段的內(nèi)存分配詳情怒坯。
另外還可以勾選同時記錄分配堆棧(Allocation stacks),也就是記錄調(diào)用堆棧藻懒,不過這會產(chǎn)生額外的性能消耗剔猿。
?? 如何開始
點擊頁面底部的 Start 按鈕或者左上角的 ? 按鈕即可開始記錄,記錄過程中點擊左上角的 ?? 按鈕來結(jié)束記錄嬉荆,片刻之后就會自動展示結(jié)果艳馒。
?? 操作一下
① 打開 Memory 面板,開始記錄分配時間軸员寇。
② 切換到 Console 面板弄慰,執(zhí)行以下代碼:
代碼效果:每隔 1 秒鐘創(chuàng)建 100 個對象,共創(chuàng)建 1000 個對象蝶锋。
console.log('測試開始');
let objects = [];
let handler = setInterval(() => {
// 每秒創(chuàng)建 100 個對象
for (let i = 0; i < 100; i++) {
const name = `n${objects.length}`;
const value = `v${objects.length}`;
objects.push({ [name]: value});
}
console.log(`對象數(shù)量:${objects.length}`);
// 達到 1000 個后停止
if (objects.length >= 1000) {
clearInterval(handler);
console.log('測試結(jié)束');
}
}, 1000);
?? 又是一個細節(jié)
不知道你有沒有發(fā)現(xiàn)陆爽,在上面的代碼中,我干了一件壞事扳缕。
在 for 循環(huán)創(chuàng)建對象時慌闭,會根據(jù)對象數(shù)組當前長度生成一個唯一的屬性名和屬性值。
這樣一來 V8 就無法對這些對象進行優(yōu)化躯舔,方便我們進行測試驴剔。
另外,如果直接使用對象數(shù)組的長度作為屬性名會有驚喜~
③ 靜靜等待 10 秒鐘粥庄,控制臺會打印出“測試結(jié)束”丧失。
④ 切換回 Memory 面板,停止記錄惜互,片刻之后會自動進入結(jié)果頁面布讹。
分配時間軸結(jié)果頁有 4 種視圖:
- Summary:摘要視圖
- Containment:包含視圖
- Allocation:分配視圖
- Statistics:統(tǒng)計視圖
默認顯示 Summary 視圖琳拭。
Summary(摘要視圖)
看起來和堆快照的摘要視圖很相似,主要是頁面上方多了一條橫向的時間軸(Timeline)描验。
?? 時間軸
時間軸中主要的 3 種線:
- 細橫線:內(nèi)存分配大小刻度線
- 藍色豎線:表示內(nèi)存在對應(yīng)時刻被分配白嘁,最后仍然活躍
- 灰色豎線:表示內(nèi)存在對應(yīng)時刻被分配,但最后被回收
時間軸的幾個操作:
- 鼠標移動到時間軸內(nèi)任意位置膘流,點擊左鍵或長按左鍵并拖動即可選擇一段時間
- 鼠標拖動時間段框上方的方塊可以對已選擇的時間段進行調(diào)整
- 鼠標移到已選擇的時間段框內(nèi)部絮缅,滑動滾輪可以調(diào)整時間范圍
- 鼠標移到已選擇的時間段框兩旁,滑動滾輪即可調(diào)整時間段
- 雙擊鼠標左鍵即可取消選擇
在時間軸中選擇要查看的時間段呼股,即可得到該段時間的內(nèi)存分配詳情盟蚣。
Containment(包含視圖)
分配時間軸的包含視圖與堆快照的包含視圖是一樣的,這里就不再重復(fù)介紹了卖怜。
Allocation(分配視圖)
對不起各位屎开,這玩意兒我也不知道有啥用...
打開就直接報錯,我:喵喵喵马靠?
是不是因為沒人用這玩意兒奄抽,所以沒人發(fā)現(xiàn)有問題...
Statistics(統(tǒng)計視圖)
分配時間軸的統(tǒng)計視圖與堆快照的統(tǒng)計視圖也是一樣的,不再贅述甩鳄。
Allocation sampling(分配采樣)
Memory 面板上的簡介:使用采樣方法記錄內(nèi)存分配逞度。這種分析方式的性能開銷最小,可以用于長時間的記錄妙啃。
好家伙档泽,這個簡介有夠模糊,說了跟沒說似的揖赴,很有精神馆匿!
我在官方文檔里沒有找到任何關(guān)于分配采樣的介紹,Google 上也幾乎沒有與之有關(guān)的信息燥滑。所以以下內(nèi)容僅為個人實踐得出的結(jié)果渐北,如有不對的地方歡迎各位指出!
簡單來說铭拧,通過分配采樣我們可以很直觀地看到代碼中的每個函數(shù)(API)所分配的內(nèi)存大小赃蛛。
由于是采樣的方式,所以結(jié)果并非百分百準確搀菩,即使每次執(zhí)行相同的操作也可能會有不同的結(jié)果呕臂,但是足以讓我們了解內(nèi)存分配的大體情況。
? 如何開始
點擊頁面底部的 Start 按鈕或者左上角的 ? 按鈕即可開始記錄肪跋,記錄過程中點擊左上角的 ?? 按鈕來結(jié)束記錄歧蒋,片刻之后就會自動展示結(jié)果。
?? 操作一下
① 打開 Memory 面板,開始記錄分配采樣疏尿。
② 切換到 Console 面板,執(zhí)行以下代碼:
代碼看起來有點長易桃,其實就是 4 個函數(shù)分別以不同的方式往數(shù)組里面添加對象褥琐。
// 普通單層調(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();
// 兩層嵌套多個調(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 面板,停止記錄晤郑,片刻之后會自動進入結(jié)果頁面敌呈。
分配采樣結(jié)果頁有 3 種視圖可選:
- Chart:圖表視圖
- Heavy (Bottom Up):扁平視圖(調(diào)用層級自下而上)
- Tree (Top Down):樹狀視圖(調(diào)用層級自上而下)
這個 Heavy 我真的不知道該怎么翻譯,所以我就按照具體表現(xiàn)來命名了造寝。
默認會顯示 Chart 視圖磕洪。
Chart(圖表視圖)
Chart 視圖以圖形化的表格形式展現(xiàn)各個函數(shù)的內(nèi)存分配詳情,可以選擇精確到內(nèi)存分配的不同階段(以內(nèi)存分配的大小為軸)诫龙。
鼠標左鍵點擊析显、拖動和雙擊以操作內(nèi)存分配階段軸(和時間軸一樣),選擇要查看的階段范圍签赃。
將鼠標移動到函數(shù)方塊上會顯示函數(shù)的內(nèi)存分配詳情谷异。
鼠標左鍵點擊函數(shù)方塊可以跳轉(zhuǎn)到相應(yīng)代碼。
Heavy(扁平視圖)
Heavy 視圖將函數(shù)調(diào)用層級壓平锦聊,函數(shù)將以獨立的個體形式展現(xiàn)歹嘹。另外也可以展開調(diào)用層級,不過是自下而上的結(jié)構(gòu)孔庭,也就是一個反向的函數(shù)調(diào)用過程尺上。
視圖中的兩種 Size(大小):
- Self Size:自身大小圆到,指的是在函數(shù)內(nèi)部直接分配的內(nèi)存空間大小怎抛。
- Total Size:總大小,指的是函數(shù)總共分配的內(nèi)存空間大小芽淡,也就是包括函數(shù)內(nèi)部嵌套調(diào)用的其他函數(shù)所分配的大小抽诉。
Tree(樹狀視圖)
Tree 視圖以樹形結(jié)構(gòu)展現(xiàn)函數(shù)調(diào)用層級。我們可以從代碼執(zhí)行的源頭開始自上而下逐層展開吐绵,呈現(xiàn)一個完整的正向的函數(shù)調(diào)用過程迹淌。
參考資料
《JavaScript 高級程序設(shè)計(第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
傳送門
更多分享
《Cocos Creator 性能優(yōu)化:DrawCall》
《在 Cocos Creator 中優(yōu)雅且高效地管理彈窗》
《Cocos Creator 源碼解讀:引擎啟動與主循環(huán)》
公眾號
菜鳥小棧
??我是陳皮皮己单,一個還在不斷學(xué)習(xí)的游戲開發(fā)者唉窃,一個熱愛分享的 Cocos Star Writer。
??這是我的個人公眾號纹笼,專注但不僅限于游戲開發(fā)和前端技術(shù)分享纹份。
??每一篇原創(chuàng)都非常用心,你的關(guān)注就是我原創(chuàng)的動力!
Input and output.