JavaScript 如何管理內(nèi)存
內(nèi)存影響性能。當(dāng)你的應(yīng)用程序代碼消耗太多內(nèi)存時(shí),你會(huì)得到一個(gè)卡頓的應(yīng)用程序,甚至可能會(huì)出現(xiàn)內(nèi)存耗盡的崩潰。
當(dāng)你在web瀏覽器中運(yùn)行一個(gè)應(yīng)用程序時(shí),大部分應(yīng)用程序內(nèi)存存儲(chǔ)在Javascript堆中,而這個(gè)堆位于計(jì)算機(jī)的RAM內(nèi)。在 Vue 或者 React 應(yīng)用程序的上下文中,存儲(chǔ)在JS堆中的常見對(duì)象包括:組件實(shí)例、虛擬DOM,以及如果你使用狀態(tài)管理工具(Vuex、Pinia、Redux、mobx)的話,包含 State、Action 等。
當(dāng)你與一個(gè)應(yīng)用程序進(jìn)行交互時(shí),JS堆可能會(huì)隨著時(shí)間的推移而增長(zhǎng)。你可以通過(guò)使用Chrome DevTools進(jìn)行剖析時(shí)觀察到這種增長(zhǎng)。
我在瀏覽 Medium 首頁(yè)幾秒鐘后拍攝了上面的截圖。
由于內(nèi)存增長(zhǎng)會(huì)影響應(yīng)用程序的性能,因此清除未使用的內(nèi)存以釋放空間非常重要。Javascript引擎回收未使用內(nèi)存的過(guò)程稱為垃圾回收(GC)。與某些語(yǔ)言(如C和C++)不同,Javascript引擎會(huì)自動(dòng)進(jìn)行垃圾回收。
讓我們從Chrome開始分析。
Javascript 與 Major GC 和 Minor GC
Major GC 和 Minor GC
Major GC( 主垃圾回收器 ):主要負(fù)責(zé)老生代的垃圾回收;
Minor GC( 副垃圾回收器 ):主要負(fù)責(zé)新生代的垃圾回收;
在瀏覽器中能找到他們(Major GC、Minor GC)之間的工作身影:
- JS HEAP SIZE 明顯降低的時(shí)候,必是Major GC在工作
- 反觀 Minor GC,則沒 Major工作這么明顯,但是Minor GC工作會(huì)比Major頻繁得多
Javascript使用 Major GC 和 Minor GC 清理JS堆
打開Chrome開發(fā)者工具,并導(dǎo)航到性能(Performance)選項(xiàng)卡,確保選中“內(nèi)存(Memory)”復(fù)選框。
這是Chrome分析器生成的配置文件:
你可以看到堆的大小會(huì)增長(zhǎng)和縮小。如果你仔細(xì)觀察,你會(huì)發(fā)現(xiàn)堆大小的下降與垃圾回收活動(dòng)是對(duì)應(yīng)的。"Major GC" 表示正在進(jìn)行一次主要的垃圾回收。
當(dāng)JS堆大小大幅下降時(shí),你很可能會(huì)在任何網(wǎng)站上看到這種模式。事實(shí)上,如果你在單頁(yè)應(yīng)用程序中切換標(biāo)簽頁(yè)時(shí),JS堆的大小沒有顯著減少,但標(biāo)簽頁(yè)的內(nèi)容有所不同,這可能是內(nèi)存泄漏的跡象。
在剖析中,你還會(huì)看到“Minor GC”幀。
這些 Major GC 和 Minor GC 幀向你揭示了一些關(guān)于Javascript中垃圾回收的信息。
- 垃圾回收有兩種類型:Major GC 和 Minor GC。
- Minor GC 通常很短暫,而 Major GC 則需要更長(zhǎng)的時(shí)間。
- 在“任務(wù)”下看到它們作為幀的事實(shí)表明它們都在主應(yīng)用程序線程上運(yùn)行。
- 因?yàn)樗鼈冊(cè)谥骶€程上運(yùn)行,它們會(huì)阻塞Javascript執(zhí)行,因?yàn)闉g覽器只有一個(gè)主線程來(lái)運(yùn)行你的Javascript代碼。
- 由于它們阻塞了Javascript執(zhí)行,過(guò)多的GC會(huì)影響應(yīng)用程序的性能。
注意:GC 只有在某些時(shí)候會(huì)阻塞Javascript執(zhí)行。它并不是一直阻塞,而是在顯示為幀時(shí)。我們?cè)贑hrome分析器中無(wú)法看到這一點(diǎn),但在Firefox分析器中可以看到。
現(xiàn)在讓我們來(lái)看看Firefox分析器,它也揭示了Major GC和Minor GC之間的差異。
Major GC和Minor GC清理JS堆的兩個(gè)不同部分
我不是Firefox的用戶,我之所以將Firefox下載到我的筆記本電腦上,僅僅是因?yàn)樗钠饰龉δ堋irefox的剖析器是開源的,并且比Chrome的剖析器有更多的可視化功能。
錄制一個(gè)剖析文件,并導(dǎo)航到“Marker Chart”可視化。Marker Chart本質(zhì)上是所有不同類型活動(dòng)的時(shí)間軸,包括垃圾回收活動(dòng)。
你可能會(huì)注意到的第一件事是,Minor GC 發(fā)生頻率相當(dāng)高,而 Major GC 則不是。這是有很好的原因的,因?yàn)槿缜懊嫠?,Minor GC 很短暫,而 Major GC 則需要更長(zhǎng)的時(shí)間。
如果你將鼠標(biāo)懸停在一個(gè) Minor GC 切片上,你會(huì)得到以下描述:
因此,除了它們以不同的頻率運(yùn)行外,Major GC 和 Minor GC 還清理JS堆的不同部分。
如上所述,Minor GC 清除了所謂的“苗圃集合(nursery collection)”。 如果對(duì)象在 Minor GC 中幸存下來(lái),它們就會(huì)從 Nursery 移動(dòng)到“終身(tenured)”堆或“長(zhǎng)期(long-lived)”堆上。 因此,Major GC 必定是清除永久堆的對(duì)象。
Major GC通過(guò)一種名為mark—and—sweep的算法來(lái)清理永久化的堆,我們稍后將介紹該算法。
現(xiàn)在我們知道 JS 堆至少有 2 個(gè)組件:
- 一個(gè)用于最近分配的對(duì)象的苗圃集合(nursery collection)
- 一個(gè)用于長(zhǎng)期存在的對(duì)象的永久堆。(“終身(tenured)”堆或“長(zhǎng)期(long-lived)”堆)
主要GC算法(增量標(biāo)記和清除)
Marker Chart 最后一行的屏幕截圖中還有一個(gè) GCSlide。 嗯,那是什么?
如果將鼠標(biāo)懸停在 Major GC 矩形上,你會(huì)看到 Major GC 的描述:
整個(gè)Major GC花費(fèi)了580毫秒。然而,正如描述所說(shuō),主線程僅在個(gè)別切片上被阻塞。因此,最后一行的GCSlide指的是這些阻塞切片。再往下看,你會(huì)發(fā)現(xiàn)總切片時(shí)間是34.4毫秒,因此在Javascript主線程無(wú)法執(zhí)行應(yīng)用程序代碼時(shí),只有34.4毫秒的工作。
如果你將鼠標(biāo)懸停在其中一個(gè)GC切片上,你會(huì)得到:
請(qǐng)注意,兩個(gè)工具提示都有“標(biāo)記階段”和“清除階段”。這意味著Javascript垃圾回收有兩個(gè)階段:標(biāo)記和清除。這個(gè)算法稱為標(biāo)記-清除。請(qǐng)注意,GC切片的描述中提到了“增量垃圾回收的一個(gè)切片”。增量GC是實(shí)現(xiàn)標(biāo)記-清除算法的一種方式。
標(biāo)記階段通過(guò)遍歷對(duì)象圖來(lái)“標(biāo)記”所有從根集合(例如全局變量、活動(dòng)函數(shù)調(diào)用)可達(dá)的對(duì)象。然后,清除階段“掃描”整個(gè)堆,收集并釋放所有未標(biāo)記的對(duì)象的內(nèi)存,這些對(duì)象被視為不可達(dá),因此是垃圾。
增量意味著你在標(biāo)記階段以增量方式進(jìn)行操作,從而最小化主線程被阻塞的時(shí)間。
Chrome vs. Firefox
當(dāng)涉及到Javascript中的垃圾回收時(shí),我想提出一些細(xì)微差別。
Chrome(以及NodeJS)使用V8 Javascript引擎。
另一方面,F(xiàn)irefox使用一個(gè)名為SpiderMonkey的不同引擎。正是Javascript引擎帶來(lái)了垃圾回收。V8使用術(shù)語(yǔ)“young generation”代替“nursery”,以及“old generation”代替“tenured heap”。然而,V8和SpiderMonkey都使用Minor GC來(lái)清理年輕對(duì)象和Major GC來(lái)清理老對(duì)象。
兩者都實(shí)現(xiàn)了增量標(biāo)記-清除算法用于Major GC。不過(guò),具體的 增量標(biāo)記-清除算法 在V8和SpiderMonkey之間有所不同。
總結(jié)一下: