引言
作為目前最流行的JavaScript引擎,V8引擎從出現的那一刻起便廣泛受到人們的關注,我們知道,JavaScript可以高效地運行在瀏覽器和Nodejs這兩大宿主環境中,也是因為背后有強大的V8引擎在為其保駕護航,甚至成就了Chrome在瀏覽器中的霸主地位,不得不說,V8引擎為了追求極致的性能和更好的用戶體驗,為我們做了太多太多,從原始的Full-codegen和Crankshaft編譯器升級為Ignition解釋器和TurboFan編譯器的強強組合,到隱藏類,行內快取和HotSpot熱點代碼收集等一系列強有力的優化策略,V8引擎正在努力降低整體的記憶體占用和提升到更高的運行性能,
本篇主要是從V8引擎的垃圾回識訓制入手,講解一下在JavaScript代碼執行的整個生命周期中V8引擎是采取怎樣的垃圾回收策略來減少記憶體占比的,當然這部分的知識并不太影響我們寫代碼的流程,畢竟在一般情況下我們很少會遇到瀏覽器端出現記憶體溢位而導致程式崩潰的情況,但是至少我們對這方面有一定的了解之后,能增強我們在寫代碼程序中對減少記憶體占用,避免記憶體泄漏的主觀意識,也許能夠幫助你寫出更加健壯和對V8引擎更加友好的代碼,本文也是筆者在查閱資料鞏固復習的程序中慢慢總結和整理出來的,若文中有錯誤的地方,還請指正,
1、為何需要垃圾回收
我們知道,在V8引擎逐行執行JavaScript代碼的程序中,當遇到函式的情況時,會為其創建一個函式執行背景關系(Context)環境并添加到呼叫堆疊的堆疊頂,函式的作用域(handleScope)中包含了該函式中宣告的所有變數,當該函式執行完畢后,對應的執行背景關系從堆疊頂彈出,函式的作用域會隨之銷毀,其包含的所有變數也會統一釋放并被自動回收,試想如果在這個作用域被銷毀的程序中,其中的變數不被回收,即持久占用記憶體,那么必然會導致記憶體暴增,從而引發記憶體泄漏導致程式的性能直線下降甚至崩潰,因此記憶體在使用完畢之后理當歸還給作業系統以保證記憶體的重復利用,
這個程序就好比你向親戚朋友借錢,借得多了卻不按時歸還,那么你再下次借錢的時候肯定沒有那么順利了,或者說你的親戚朋友不愿意再借你了,導致你的手頭有點兒緊(記憶體泄漏,性能下降),所以說有借有還,再借不難嘛,畢竟出來混都是要還的,
但是JavaScript作為一門高級編程語言,并不像C語言或C++語言中需要手動地申請分配和釋放記憶體,V8引擎已經幫我們自動進行了記憶體的分配和管理,好讓我們有更多的精力去專注于業務層面的復雜邏輯,這對于我們前端開發人員來說是一項福利,但是隨之帶來的問題也是顯而易見的,那就是由于不用去手動管理記憶體,導致寫代碼的程序中不夠嚴謹從而容易引發記憶體泄漏(畢竟這是別人對你的好,你沒有付出過,又怎能體會得到?),
2、V8引擎的記憶體限制
雖然V8引擎幫助我們實作了自動的垃圾回收管理,解放了我們勤勞的雙手,但V8引擎中的記憶體使用也并不是無限制的,具體來說,默認情況下,V8引擎在64位系統下最多只能使用約1.4GB的記憶體,在32位系統下最多只能使用約0.7GB的記憶體,在這樣的限制下,必然會導致在node中無法直接操作大記憶體物件,比如將一個2GB大小的檔案全部讀入記憶體進行字串分析處理,即使物理記憶體高達32GB也無法充分利用計算機的記憶體資源,那么為什么會有這種限制呢?這個要回到V8引擎的設計之初,起初只是作為瀏覽器端JavaScript的執行環境,在瀏覽器端我們其實很少會遇到使用大量記憶體的場景,因此也就沒有必要將最大記憶體設定得過高,但這只是一方面,其實還有另外兩個主要的原因:
JS單執行緒機制:作為瀏覽器的腳本語言,JS的主要用途是與用戶互動以及操作DOM,那么這也決定了其作為單執行緒的本質,單執行緒意味著執行的代碼必須按順序執行,在同一時間只能處理一個任務,試想如果JS是多執行緒的,一個執行緒在洗掉DOM元素的同時,另一個執行緒對該元素進行修改操作,那么必然會導致復雜的同步問題,既然JS是單執行緒的,那么也就意味著在V8執行垃圾回收時,程式中的其他各種邏輯都要進入暫停等待階段,直到垃圾回收結束后才會再次重新執行JS邏輯,因此,由于JS的單執行緒機制,垃圾回收的程序阻礙了主執行緒邏輯的執行,
雖然JS是單執行緒的,但是為了能夠充分利用作業系統的多核CPU計算能力,在HTML5中引入了新的Web Worker標準,其作用就是為JS創造多執行緒環境,允許主執行緒創建Worker執行緒,將一些任務分配給后者運行,在主執行緒運行的同時,Worker在后臺運行,兩者互不干擾,等到Worker執行緒完成計算任務,再把結果回傳給主執行緒,這樣的好處是, 一些計算密集型或高延遲的任務,被Worker執行緒負擔,主執行緒(通常負責UI互動)就會很流暢,不會被阻塞或者拖慢,Web Worker不是JS的一部分,而是通過JS訪問的瀏覽器特性,其雖然創造了一個多執行緒的執行環境,但是子執行緒完全受主執行緒控制,不能訪問瀏覽器特定的API,例如操作DOM,因此這個新標準并沒有改變JS單執行緒的本質,
垃圾回識訓制:垃圾回收本身也是一件非常耗時的操作,假設V8的堆記憶體為1.5G,那么V8做一次小的垃圾回收需要50ms以上,而做一次非增量式回收甚至需要1s以上,可見其耗時之久,而在這1s的時間內,瀏覽器一直處于等待的狀態,同時會失去對用戶的回應,如果有影片正在運行,也會造成影片卡頓掉幀的情況,嚴重影回應用程式的性能,因此如果記憶體使用過高,那么必然會導致垃圾回收的程序緩慢,也就會導致主執行緒的等待時間越長,瀏覽器也就越長時間得不到回應,
基于以上兩點,V8引擎為了減少對應用的性能造成的影響,采用了一種比較粗暴的手段,那就是直接限制堆記憶體的大小,畢竟在瀏覽器端一般也不會遇到需要操作幾個G記憶體這樣的場景,但是在node端,涉及到的I/O操作可能會比瀏覽器端更加復雜多樣,因此更有可能出現記憶體溢位的情況,不過也沒關系,V8為我們提供了可配置項來讓我們手動地調整記憶體大小,但是需要在node初始化的時候進行配置,我們可以通過如下方式來手動設定,
我們嘗試在node命令列中輸入以下命令:
筆者本地安裝的node版本為
v10.14.2,可通過node -v查看本地node的版本號,不同版本可能會導致下面的命令會有所差異,
// 該命令可以用來查看node中可用的V8引擎的選項及其含義
node --v8-options
然后我們會在命令列視窗中看到大量關于V8的選項,這里我們暫且只關注圖中紅色選框中的幾個選項:
// 設定新生代記憶體中單個半空間的記憶體最小值,單位MB
node --min-semi-space-size=1024 xxx.js
// 設定新生代記憶體中單個半空間的記憶體最大值,單位MB
node --max-semi-space-size=1024 xxx.js
// 設定老生代記憶體最大值,單位MB
node --max-old-space-size=2048 xxx.js
通過以上方法便可以手動放寬V8引擎所使用的記憶體限制,同時node也為我們提供了process.memoryUsage()方法來讓我們可以查看當前node行程所占用的實際記憶體大小,
在上圖中,包含的幾個欄位的含義分別如下所示,單位均為位元組:
heapTotal:表示V8當前申請到的堆記憶體總大小,heapUsed:表示當前記憶體使用量,external:表示V8內部的C++物件所占用的記憶體,rss(resident set size):表示駐留集大小,是給這個node行程分配了多少物理記憶體,這些物理記憶體中包含堆,堆疊和代碼片段,物件,閉包等存于堆記憶體,變數存于堆疊記憶體,實際的JavaScript源代碼存于代碼段記憶體,使用Worker執行緒時,rss將會是一個對整個行程有效的值,而其他欄位則只針對當前執行緒,
在JS中宣告物件時,該物件的記憶體就分配在堆中,如果當前已申請的堆記憶體已經不夠分配新的物件,則會繼續申請堆記憶體直到堆的大小超過V8的限制為止,
3、V8的垃圾回收策略
V8的垃圾回收策略主要是基于分代式垃圾回識訓制,其根據物件的存活時間將記憶體的垃圾回收進行不同的分代,然后對不同的分代采用不同的垃圾回收演算法,
3.1 V8的記憶體結構
在V8引擎的堆結構組成中,其實除了新生代和老生代外,還包含其他幾個部分,但是垃圾回收的程序主要出現在新生代和老生代,所以對于其他的部分我們沒必要做太多的深入,有興趣的小伙伴兒可以查閱下相關資料,V8的記憶體結構主要由以下幾個部分組成:
新生代(new_space):大多數的物件開始都會被分配在這里,這個區域相對較小但是垃圾回收特別頻繁,該區域被分為兩半,一半用來分配記憶體,另一半用于在垃圾回收時將需要保留的物件復制過來,老生代(old_space):新生代中的物件在存活一段時間后就會被轉移到老生代記憶體區,相對于新生代該記憶體區域的垃圾回收頻率較低,老生代又分為老生代指標區和老生代資料區,前者包含大多數可能存在指向其他物件的指標的物件,后者只保存原始資料物件,這些物件沒有指向其他物件的指標,大物件區(large_object_space):存放體積超越其他區域大小的物件,每個物件都會有自己的記憶體,垃圾回收不會移動大物件區,代碼區(code_space):代碼物件,會被分配在這里,唯一擁有執行權限的記憶體區域,map區(map_space):存放Cell和Map,每個區域都是存放相同大小的元素,結構簡單(這里沒有做具體深入的了解,有清楚的小伙伴兒還麻煩解釋下),
記憶體結構圖如下所示:
上圖中的帶斜紋的區域代表暫未使用的記憶體,新生代(new_space)被劃分為了兩個部分,其中一部分叫做inactive new space,表示暫未激活的記憶體區域,另一部分為激活狀態,為什么會劃分為兩個部分呢,在下一小節我們會講到,
3.2 新生代
在V8引擎的記憶體結構中,新生代主要用于存放存活時間較短的物件,新生代記憶體是由兩個semispace(半空間)構成的,記憶體最大值在64位系統和32位系統上分別為32MB和16MB,在新生代的垃圾回收程序中主要采用了Scavenge演算法,
Scavenge演算法是一種典型的犧牲空間換取時間的演算法,對于老生代記憶體來說,可能會存盤大量物件,如果在老生代中使用這種演算法,勢必會造成記憶體資源的浪費,但是在新生代記憶體中,大部分物件的生命周期較短,在時間效率上表現可觀,所以還是比較適合這種演算法,
在
Scavenge演算法的具體實作中,主要采用了Cheney演算法,它將新生代記憶體一分為二,每一個部分的空間稱為semispace,也就是我們在上圖中看見的new_space中劃分的兩個區域,其中處于激活狀態的區域我們稱為From空間,未激活(inactive new space)的區域我們稱為To空間,這兩個空間中,始終只有一個處于使用狀態,另一個處于閑置狀態,我們的程式中宣告的物件首先會被分配到From空間,當進行垃圾回收時,如果From空間中尚有存活物件,則會被復制到To空間進行保存,非存活的物件會被自動回收,當復制完成后,From空間和To空間完成一次角色互換,To空間會變為新的From空間,原來的From空間則變為To空間,
基于以上演算法,我們可以畫出如下的流程圖:
- 假設我們在
From空間中分配了三個物件A、B、C
- 當程式主執行緒任務第一次執行完畢后進入垃圾回收時,發現物件A已經沒有其他參考,則表示可以對其進行回收
- 物件B和物件C此時依舊處于活躍狀態,因此會被復制到
To空間中進行保存
- 接下來將
From空間中的所有非存活物件全部清除
- 此時
From空間中的記憶體已經清空,開始和To空間完成一次角色互換
- 當程式主執行緒在執行第二個任務時,在
From空間中分配了一個新物件D
- 任務執行完畢后再次進入垃圾回收,發現物件D已經沒有其他參考,表示可以對其進行回收
- 物件B和物件C此時依舊處于活躍狀態,再次被復制到
To空間中進行保存
- 再次將
From空間中的所有非存活物件全部清除
From空間和To空間繼續完成一次角色互換
通過以上的流程圖,我們可以很清楚地看到,Scavenge演算法的垃圾回收程序主要就是將存活物件在From空間和To空間之間進行復制,同時完成兩個空間之間的角色互換,因此該演算法的缺點也比較明顯,浪費了一半的記憶體用于復制,
3.3 物件晉升
當一個物件在經過多次復制之后依舊存活,那么它會被認為是一個生命周期較長的物件,在下一次進行垃圾回收時,該物件會被直接轉移到老生代中,這種物件從新生代轉移到老生代的程序我們稱之為晉升,
物件晉升的條件主要有以下兩個:
- 物件是否經歷過一次
Scavenge演算法 To空間的記憶體占比是否已經超過25%
默認情況下,我們創建的物件都會分配在From空間中,當進行垃圾回收時,在將物件從From空間復制到To空間之前,會先檢查該物件的記憶體地址來判斷是否已經經歷過一次Scavenge演算法,如果地址已經發生變動則會將該物件轉移到老生代中,不會再被復制到To空間,可以用以下的流程圖來表示:
如果物件沒有經歷過Scavenge演算法,會被復制到To空間,但是如果此時To空間的記憶體占比已經超過25%,則該物件依舊會被轉移到老生代,如下圖所示:
之所以有25%的記憶體限制是因為To空間在經歷過一次Scavenge演算法后會和From空間完成角色互換,會變為From空間,后續的記憶體分配都是在From空間中進行的,如果記憶體使用過高甚至溢位,則會影響后續物件的分配,因此超過這個限制之后物件會被直接轉移到老生代來進行管理,
3.4 老生代
在老生代中,因為管理著大量的存活物件,如果依舊使用Scavenge演算法的話,很明顯會浪費一半的記憶體,因此已經不再使用Scavenge演算法,而是采用新的演算法Mark-Sweep(標記清除)和Mark-Compact(標記整理)來進行管理,
在早前我們可能聽說過一種演算法叫做參考計數,該演算法的原理比較簡單,就是看物件是否還有其他參考指向它,如果沒有指向該物件的參考,則該物件會被視為垃圾并被垃圾回收器回收,示例如下:
// 創建了兩個物件obj1和obj2,其中obj2作為obj1的屬性被obj1參考,因此不會被垃圾回收
let obj1 = {
obj2: {
a: 1
}
}
// 創建obj3并將obj1賦值給obj3,讓兩個物件指向同一個記憶體地址
let obj3 = obj1;
// 將obj1重新賦值,此時原來obj1指向的物件現在只由obj3來表示
obj1 = null;
// 創建obj4并將obj3.obj2賦值給obj4
// 此時obj2所指向的物件有兩個參考:一個是作為obj3的屬性,另一個是變數obj4
let obj4 = obj3.obj2;
// 將obj3重新賦值,此時本可以對obj3指向的物件進行回收,但是因為obj3.obj2被obj4所參考,因此依舊不能被回收
obj3 = null;
// 此時obj3.obj2已經沒有指向它的參考,因此obj3指向的物件在此時可以被回收
obj4 = null;
上述例子在經過一系列操作后最終物件會被垃圾回收,但是一旦我們碰到回圈參考的場景,就會出現問題,我們看下面的例子:
function foo() {
let a = {};
let b = {};
a.a1 = b;
b.b1 = a;
}
foo();
這個例子中我們將物件a的a1屬性指向物件b,將物件b的b1屬性指向物件a,形成兩個物件相互參考,在foo函式執行完畢后,函式的作用域已經被銷毀,作用域中包含的變數a和b 本應該可以被回收,但是因為采用了參考計數的演算法,兩個變數均存在指向自身的參考,因此依舊無法被回收,導致記憶體泄漏,
因此為了避免回圈參考導致的記憶體泄漏問題,截至2012年所有的現代瀏覽器均放棄了這種演算法,轉而采用新的Mark-Sweep(標記清除)和Mark-Compact(標記整理)演算法,在上面回圈參考的例子中,因為變數a和變數b無法從window全域物件訪問到,因此無法對其進行標記,所以最侄訓被回收,
Mark-Sweep(標記清除)分為標記和清除兩個階段,在標記階段會遍歷堆中的所有物件,然后標記活著的物件,在清除階段中,會將死亡的物件進行清除,Mark-Sweep演算法主要是通過判斷某個物件是否可以被訪問到,從而知道該物件是否應該被回收,具體步驟如下:
- 垃圾回收器會在內部構建一個
根串列,用于從根節點出發去尋找那些可以被訪問到的變數,比如在JavaScript中,window全域物件可以看成一個根節點, - 然后,垃圾回收器從所有根節點出發,遍歷其可以訪問到的子節點,并將其標記為活動的,根節點不能到達的地方即為非活動的,將會被視為垃圾,
- 最后,垃圾回收器將會釋放所有非活動的記憶體塊,并將其歸還給作業系統,
以下幾種情況都可以作為根節點:
- 全域物件
- 本地函式的區域變數和引數
- 當前嵌套呼叫鏈上的其他函式的變數和引數
但是Mark-Sweep演算法存在一個問題,就是在經歷過一次標記清除后,記憶體空間可能會出現不連續的狀態,因為我們所清理的物件的記憶體地址可能不是連續的,所以就會出現記憶體碎片的問題,導致后面如果需要分配一個大物件而空閑記憶體不足以分配,就會提前觸發垃圾回收,而這次垃圾回收其實是沒必要的,因為我們確實有很多空閑記憶體,只不過是不連續的,
為了解決這種記憶體碎片的問題,Mark-Compact(標記整理)演算法被提了出來,該演算法主要就是用來解決記憶體的碎片化問題的,回收程序中將死亡物件清除后,在整理的程序中,會將活動的物件往堆記憶體的一端進行移動,移動完成后再清理掉邊界外的全部記憶體,我們可以用如下流程圖來表示:
- 假設在老生代中有A、B、C、D四個物件
- 在垃圾回收的
標記階段,將物件A和物件C標記為活動的
- 在垃圾回收的
整理階段,將活動的物件往堆記憶體的一端移動
- 在垃圾回收的
清除階段,將活動物件左側的記憶體全部回收
至此就完成了一次老生代垃圾回收的全部程序,我們在前文中說過,由于JS的單執行緒機制,垃圾回收的程序會阻礙主執行緒同步任務的執行,待執行完垃圾回收后才會再次恢復執行主任務的邏輯,這種行為被稱為全停頓(stop-the-world),在標記階段同樣會阻礙主執行緒的執行,一般來說,老生代會保存大量存活的物件,如果在標記階段將整個堆記憶體遍歷一遍,那么勢必會造成嚴重的卡頓,
因此,為了減少垃圾回收帶來的停頓時間,V8引擎又引入了Incremental Marking(增量標記)的概念,即將原本需要一次性遍歷堆記憶體的操作改為增量標記的方式,先標記堆記憶體中的一部分物件,然后暫停,將執行權重新交給JS主執行緒,待主執行緒任務執行完畢后再從原來暫圖示記的地方繼續標記,直到標記完整個堆記憶體,這個理念其實有點像React框架中的Fiber架構,只有在瀏覽器的空閑時間才會去遍歷Fiber Tree執行對應的任務,否則延遲執行,盡可能少地影響主執行緒的任務,避免應用卡頓,提升應用性能,
得益于增量標記的好處,V8引擎后續繼續引入了延遲清理(lazy sweeping)和增量式整理(incremental compaction),讓清理和整理的程序也變成增量式的,同時為了充分利用多核CPU的性能,也將引入并行標記和并行清理,進一步地減少垃圾回收對主執行緒的影響,為應用提升更多的性能,
4、如何避免記憶體泄漏
在我們寫代碼的程序中,基本上都不太會關注寫出怎樣的代碼才能有效地避免記憶體泄漏,或者說瀏覽器和大部分的前端框架在底層已經幫助我們處理了常見的記憶體泄漏問題,但是我們還是有必要了解一下常見的幾種避免記憶體泄漏的方式,畢竟在面試程序中也是經常考察的要點,
4.1 盡可能少地創建全域變數
在ES5中以var宣告的方式在全域作用域中創建一個變數時,或者在函式作用域中不以任何宣告的方式創建一個變數時,都會無形地掛載到window全域物件上,如下所示:
var a = 1; // 等價于 window.a = 1;
function foo() {
a = 1;
}
等價于
function foo() {
window.a = 1;
}
我們在foo函式中創建了一個變數a但是忘記使用var來宣告,此時會意想不到地創建一個全域變數并掛載到window物件上,另外還有一種比較隱蔽的方式來創建全域變數:
function foo() {
this.a = 1;
}
foo(); // 相當于 window.foo()
當foo函式在呼叫時,它所指向的運行背景關系環境為window全域物件,因此函式中的this指向的其實是window,也就無意創建了一個全域變數,當進行垃圾回收時,在標記階段因為window物件可以作為根節點,在window上掛載的屬性均可以被訪問到,并將其標記為活動的從而常駐記憶體,因此也就不會被垃圾回收,只有在整個行程退出時全域作用域才會被銷毀,如果你遇到需要必須使用全域變數的場景,那么請保證一定要在全域變數使用完畢后將其設定為null從而觸發回識訓制,
4.2 手動清除定時器
在我們的應用中經常會有使用setTimeout或者setInterval等定時器的場景,定時器本身是一個非常有用的功能,但是如果我們稍不注意,忘記在適當的時間手動清除定時器,那么很有可能就會導致記憶體泄漏,示例如下:
const numbers = [];
const foo = function() {
for(let i = 0;i < 100000;i++) {
numbers.push(i);
}
};
window.setInterval(foo, 1000);
在這個示例中,由于我們沒有手動清除定時器,導致回呼任務會不斷地執行下去,回呼中所參考的numbers變數也不會被垃圾回收,最終導致numbers陣列長度無限遞增,從而引發記憶體泄漏,
4.3 少用閉包
閉包是JS中的一個高級特性,巧妙地利用閉包可以幫助我們實作很多高級功能,一般來說,我們在查找變數時,在本地作用域中查找不到就會沿著作用域鏈從內向外單向查找,但是閉包的特性可以讓我們在外部作用域訪問內部作用域中的變數,示例如下:
function foo() {
let local = 123;
return function() {
return local;
}
}
const bar = foo();
console.log(bar()); // -> 123
在這個示例中,foo函式執行完畢后會回傳一個匿名函式,該函式內部參考了foo函式中的區域變數local,并且通過變數bar來參考這個匿名的函式定義,通過這種閉包的方式我們就可以在foo函式的外部作用域中訪問到它的區域變數local,一般情況下,當foo函式執行完畢后,它的作用域會被銷毀,但是由于存在變數參考其回傳的匿名函式,導致作用域無法得到釋放,也就導致local變數無法回收,只有當我們取消掉對匿名函式的參考才會進入垃圾回收階段,
4.4 清除DOM參考
以往我們在操作DOM元素時,為了避免多次獲取DOM元素,我們會將DOM元素存盤在一個資料字典中,示例如下:
const elements = {
button: document.getElementById('button')
};
function removeButton() {
document.body.removeChild(document.getElementById('button'));
}
在這個示例中,我們想呼叫removeButton方法來清除button元素,但是由于在elements字典中存在對button元素的參考,所以即使我們通過removeChild方法移除了button元素,它其實還是依舊存盤在記憶體中無法得到釋放,只有我們手動清除對button元素的參考才會被垃圾回收,
4.5 弱參考
通過前幾個示例我們會發現如果我們一旦疏忽,就會容易地引發記憶體泄漏的問題,為此,在ES6中為我們新增了兩個有效的資料結構WeakMap和WeakSet,就是為了解決記憶體泄漏的問題而誕生的,其表示弱參考,它的鍵名所參考的物件均是弱參考,弱參考是指垃圾回收的程序中不會將鍵名對該物件的參考考慮進去,只要所參考的物件沒有其他的參考了,垃圾回識訓制就會釋放該物件所占用的記憶體,這也就意味著我們不需要關心WeakMap中鍵名對其他物件的參考,也不需要手動地進行參考清除,我們嘗試在node中演示一下程序(參考阮一峰ES6標準入門中的示例,自己手動實作了一遍),
首先打開node命令列,輸入以下命令:
node --expose-gc // --expose-gc 表示允許手動執行垃圾回識訓制
然后我們執行下面的代碼,
// 手動執行一次垃圾回收保證記憶體資料準確
> global.gc();
undefined
// 查看當前占用的記憶體,主要關心heapUsed欄位,大小約為4.4MB
> process.memoryUsage();
{ rss: 21626880,
heapTotal: 7585792,
heapUsed: 4708440,
external: 8710 }
// 創建一個WeakMap
> let wm = new WeakMap();
undefined
// 創建一個陣列并賦值給變數key
> let key = new Array(1000000);
undefined
// 將WeakMap的鍵名指向該陣列
// 此時該陣列存在兩個參考,一個是key,一個是WeakMap的鍵名
// 注意WeakMap是弱參考
> wm.set(key, 1);
WeakMap { [items unknown] }
// 手動執行一次垃圾回收
> global.gc();
undefined
// 再次查看記憶體占用大小,heapUsed已經增加到約12MB
> process.memoryUsage();
{ rss: 30232576,
heapTotal: 17694720,
heapUsed: 13068464,
external: 8688 }
// 手動清除變數key對陣列的參考
// 注意這里并沒有清除WeakMap中鍵名對陣列的參考
> key = null;
null
// 再次執行垃圾回收
> global.gc()
undefined
// 查看記憶體占用大小,發現heapUsed已經回到了之前的大小(這里約為4.8M,原來為4.4M,稍微有些浮動)
> process.memoryUsage();
{ rss: 22110208,
heapTotal: 9158656,
heapUsed: 5089752,
external: 8698 }
在上述示例中,我們發現雖然我們沒有手動清除WeakMap中的鍵名對陣列的參考,但是記憶體依舊已經回到原始的大小,說明該陣列已經被回收,那么這個也就是弱參考的具體含義了,
5、總結
本文中主要講解了一下V8引擎的垃圾回識訓制,并分別從新生代和老生代講述了不同分代中的垃圾回收策略以及對應的回收演算法,之后列出了幾種常見的避免記憶體泄漏的方式來幫助我們寫出更加優雅的代碼,如果你已經了解過垃圾回收相關的內容,那么這篇文章可以幫助你簡單復習加深印象,如果沒有了解過,那么筆者也希望這篇文章能夠幫助到你了解一些代碼層面之外的底層知識點,由于V8引擎的原始碼是用C++實作的,所以筆者也就沒有做這方面的深入了,有興趣的小伙伴兒可以自行探究,文中有錯誤的地方,還希望能夠在評論區指正,
6、交流
如果你覺得這篇文章的內容對你有幫助,能否幫個忙關注一下筆者的公眾號[前端之境],每周都會努力原創一些前端技術干貨,關注公眾號后可以邀你加入前端技術交流群,我們可以一起互相交流,共同進步,
文章已同步更新至Github博客,若覺文章尚可,歡迎前往star!
你的一個點贊,值得讓我付出更多的努力!
逆境中成長,只有不斷地學習,才能成為更好的自己,與君共勉!

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/163651.html
標籤:JavaScript
上一篇:JS---BOM---定時器
下一篇:JS頁面跳轉加密解密URL引數
