做爲目前最流行的JavaScript引擎,V8引擎從出現的那一刻起便普遍受到人們的關注,咱們知道,JavaScript能夠高效地運行在瀏覽器和Nodejs這兩大宿主環境中,也是由於背後有強大的V8引擎在爲其保駕護航,甚至成就了Chrome在瀏覽器中的霸主地位。不得不說,V8引擎爲了追求極致的性能和更好的用戶體驗,爲咱們作了太多太多,從原始的Full-codegen
和Crankshaft
編譯器升級爲Ignition
解釋器和TurboFan
編譯器的強強組合,到隱藏類,內聯緩存和HotSpot
熱點代碼收集等一系列強有力的優化策略,V8引擎正在努力下降總體的內存佔用和提高到更高的運行性能。javascript
本篇主要是從V8引擎的垃圾回收機制入手,講解一下在JavaScript代碼執行的整個生命週期中V8引擎是採起怎樣的垃圾回收策略來減小內存佔比的,固然這部分的知識並不太影響咱們寫代碼的流程,畢竟在通常狀況下咱們不多會遇到瀏覽器端出現內存溢出而致使程序崩潰的狀況,可是至少咱們對這方面有必定的瞭解以後,能加強咱們在寫代碼過程當中對減小內存佔用,避免內存泄漏的主觀意識,也許可以幫助你寫出更加健壯和對V8引擎更加友好的代碼。本文也是筆者在查閱資料鞏固複習的過程當中慢慢總結和整理出來的,若文中有錯誤的地方,還請指正。前端
咱們知道,在V8引擎逐行執行JavaScript代碼的過程當中,當遇到函數的狀況時,會爲其建立一個函數執行上下文(Context)環境並添加到調用堆棧的棧頂,函數的做用域(handleScope)中包含了該函數中聲明的全部變量,當該函數執行完畢後,對應的執行上下文從棧頂彈出,函數的做用域會隨之銷燬,其包含的全部變量也會統一釋放並被自動回收。試想若是在這個做用域被銷燬的過程當中,其中的變量不被回收,即持久佔用內存,那麼必然會致使內存暴增,從而引起內存泄漏致使程序的性能直線降低甚至崩潰,所以內存在使用完畢以後理當歸還給操做系統以保證內存的重複利用。java
這個過程就比如你向親戚朋友借錢,借得多了卻不按時歸還,那麼你再下次借錢的時候確定沒有那麼順利了,或者說你的親戚朋友不肯意再借你了,致使你的手頭有點兒緊(內存泄漏,性能降低),因此說有借有還,再借不難嘛,畢竟出來混都是要還的。node
可是JavaScript做爲一門高級編程語言,並不像C語言或C++語言中須要手動地申請分配和釋放內存,V8引擎已經幫咱們自動進行了內存的分配和管理,好讓咱們有更多的精力去專一於業務層面的複雜邏輯,這對於咱們前端開發人員來講是一項福利,可是隨之帶來的問題也是顯而易見的,那就是因爲不用去手動管理內存,致使寫代碼的過程當中不夠嚴謹從而容易引起內存泄漏(畢竟這是別人對你的好,你沒有付出過,又怎能體會獲得?)。git
雖然V8引擎幫助咱們實現了自動的垃圾回收管理,解放了咱們勤勞的雙手,但V8引擎中的內存使用也並非無限制的。具體來講,默認狀況下,V8引擎在64
位系統下最多隻能使用約1.4GB
的內存,在32
位系統下最多隻能使用約0.7GB
的內存,在這樣的限制下,必然會致使在node中沒法直接操做大內存對象,好比將一個2GB
大小的文件所有讀入內存進行字符串分析處理,即便物理內存高達32GB
也沒法充分利用計算機的內存資源,那麼爲何會有這種限制呢?這個要回到V8引擎的設計之初,起初只是做爲瀏覽器端JavaScript的執行環境,在瀏覽器端咱們其實不多會遇到使用大量內存的場景,所以也就沒有必要將最大內存設置得太高。但這只是一方面,其實還有另外兩個主要的緣由:github
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的限制爲止。
V8的垃圾回收策略主要是基於分代式垃圾回收機制
,其根據對象的存活時間將內存的垃圾回收進行不一樣的分代,而後對不一樣的分代採用不一樣的垃圾回收算法。
在V8引擎的堆結構組成中,其實除了新生代
和老生代
外,還包含其餘幾個部分,可是垃圾回收的過程主要出如今新生代和老生代,因此對於其餘的部分咱們不必作太多的深刻,有興趣的小夥伴兒能夠查閱下相關資料,V8的內存結構主要由如下幾個部分組成:
新生代(new_space)
:大多數的對象開始都會被分配在這裏,這個區域相對較小可是垃圾回收特別頻繁,該區域被分爲兩半,一半用來分配內存,另外一半用於在垃圾回收時將須要保留的對象複製過來。老生代(old_space)
:新生代中的對象在存活一段時間後就會被轉移到老生代內存區,相對於新生代該內存區域的垃圾回收頻率較低。老生代又分爲老生代指針區
和老生代數據區
,前者包含大多數可能存在指向其餘對象的指針的對象,後者只保存原始數據對象,這些對象沒有指向其餘對象的指針。大對象區(large_object_space)
:存放體積超越其餘區域大小的對象,每一個對象都會有本身的內存,垃圾回收不會移動大對象區。代碼區(code_space)
:代碼對象,會被分配在這裏,惟一擁有執行權限的內存區域。map區(map_space)
:存放Cell和Map,每一個區域都是存放相同大小的元素,結構簡單(這裏沒有作具體深刻的瞭解,有清楚的小夥伴兒還麻煩解釋下)。內存結構圖以下所示:
上圖中的帶斜紋的區域表明暫未使用的內存,新生代(new_space)被劃分爲了兩個部分,其中一部分叫作inactive new space,表示暫未激活的內存區域,另外一部分爲激活狀態,爲何會劃分爲兩個部分呢,在下一小節咱們會講到。在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、CTo
空間中進行保存From
空間中的全部非存活對象所有清除From
空間中的內存已經清空,開始和To
空間完成一次角色互換From
空間中分配了一個新對象DTo
空間中進行保存From
空間中的全部非存活對象所有清除From
空間和To
空間繼續完成一次角色互換Scavenge
算法的垃圾回收過程主要就是將存活對象在
From
空間和
To
空間之間進行復制,同時完成兩個空間之間的角色互換,所以該算法的缺點也比較明顯,浪費了一半的內存用於複製。
當一個對象在通過屢次複製以後依舊存活,那麼它會被認爲是一個生命週期較長的對象,在下一次進行垃圾回收時,該對象會被直接轉移到老生代中,這種對象重新生代轉移到老生代的過程咱們稱之爲晉升
。
對象晉升的條件主要有如下兩個:
Scavenge
算法To
空間的內存佔比是否已經超過25%
默認狀況下,咱們建立的對象都會分配在From
空間中,當進行垃圾回收時,在將對象從From
空間複製到To
空間以前,會先檢查該對象的內存地址來判斷是否已經經歷過一次Scavenge
算法,若是地址已經發生變更則會將該對象轉移到老生代中,不會再被複制到To
空間,能夠用如下的流程圖來表示:
Scavenge
算法,會被複制到
To
空間,可是若是此時
To
空間的內存佔比已經超過
25%
,則該對象依舊會被轉移到老生代,以下圖所示:
之因此有
25%
的內存限制是由於
To
空間在經歷過一次
Scavenge
算法後會和
From
空間完成角色互換,會變爲
From
空間,後續的內存分配都是在
From
空間中進行的,若是內存使用太高甚至溢出,則會影響後續對象的分配,所以超過這個限制以後對象會被直接轉移到老生代來進行管理。
在老生代中,由於管理着大量的存活對象,若是依舊使用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和對象C標記爲活動的整理
階段,將活動的對象往堆內存的一端移動清除
階段,將活動對象左側的內存所有回收至此就完成了一次老生代垃圾回收的所有過程,咱們在前文中說過,因爲JS的單線程機制,垃圾回收的過程會阻礙主線程同步任務的執行,待執行完垃圾回收後纔會再次恢復執行主任務的邏輯,這種行爲被稱爲全停頓(stop-the-world)
。在標記階段一樣會阻礙主線程的執行,通常來講,老生代會保存大量存活的對象,若是在標記階段將整個堆內存遍歷一遍,那麼勢必會形成嚴重的卡頓。
所以,爲了減小垃圾回收帶來的停頓時間,V8引擎又引入了Incremental Marking(增量標記)
的概念,即將本來須要一次性遍歷堆內存的操做改成增量標記的方式,先標記堆內存中的一部分對象,而後暫停,將執行權從新交給JS主線程,待主線程任務執行完畢後再從原來暫停標記的地方繼續標記,直到標記完整個堆內存。這個理念其實有點像React
框架中的Fiber
架構,只有在瀏覽器的空閒時間纔會去遍歷Fiber Tree
執行對應的任務,不然延遲執行,儘量少地影響主線程的任務,避免應用卡頓,提高應用性能。
得益於增量標記的好處,V8引擎後續繼續引入了延遲清理(lazy sweeping)
和增量式整理(incremental compaction)
,讓清理和整理的過程也變成增量式的。同時爲了充分利用多核CPU的性能,也將引入並行標記
和並行清理
,進一步地減小垃圾回收對主線程的影響,爲應用提高更多的性能。
在咱們寫代碼的過程當中,基本上都不太會關注寫出怎樣的代碼纔能有效地避免內存泄漏,或者說瀏覽器和大部分的前端框架在底層已經幫助咱們處理了常見的內存泄漏問題,可是咱們仍是有必要了解一下常見的幾種避免內存泄漏的方式,畢竟在面試過程當中也是常常考察的要點。
在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
從而觸發回收機制。
在咱們的應用中常常會有使用setTimeout
或者setInterval
等定時器的場景,定時器自己是一個很是有用的功能,可是若是咱們稍不注意,忘記在適當的時間手動清除定時器,那麼頗有可能就會致使內存泄漏,示例以下:
const numbers = [];
const foo = function() {
for(let i = 0;i < 100000;i++) {
numbers.push(i);
}
};
window.setInterval(foo, 1000);
複製代碼
在這個示例中,因爲咱們沒有手動清除定時器,致使回調任務會不斷地執行下去,回調中所引用的numbers
變量也不會被垃圾回收,最終致使numbers
數組長度無限遞增,從而引起內存泄漏。
閉包是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
變量沒法回收,只有當咱們取消掉對匿名函數的引用纔會進入垃圾回收階段。
以往咱們在操做DOM元素時,爲了不屢次獲取DOM元素,咱們會將DOM元素存儲在一個數據字典中,示例以下:
const elements = {
button: document.getElementById('button')
};
function removeButton() {
document.body.removeChild(document.getElementById('button'));
}
複製代碼
在這個示例中,咱們想調用removeButton
方法來清除button
元素,可是因爲在elements
字典中存在對button
元素的引用,因此即便咱們經過removeChild
方法移除了button
元素,它其實仍是依舊存儲在內存中沒法獲得釋放,只有咱們手動清除對button
元素的引用纔會被垃圾回收。
經過前幾個示例咱們會發現若是咱們一旦疏忽,就會容易地引起內存泄漏的問題,爲此,在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
中的鍵名對數組的引用,可是內存依舊已經回到原始的大小,說明該數組已經被回收,那麼這個也就是弱引用的具體含義了。
本文中主要講解了一下V8引擎的垃圾回收機制,並分別重新生代和老生代講述了不一樣分代中的垃圾回收策略以及對應的回收算法,以後列出了幾種常見的避免內存泄漏的方式來幫助咱們寫出更加優雅的代碼。若是你已經瞭解過垃圾回收相關的內容,那麼這篇文章能夠幫助你簡單複習加深印象,若是沒有了解過,那麼筆者也但願這篇文章可以幫助到你瞭解一些代碼層面以外的底層知識點,因爲V8引擎的源碼是用C++實現的,因此筆者也就沒有作這方面的深刻了,有興趣的小夥伴兒能夠自行探究,文中有錯誤的地方,還但願可以在評論區指正。
若是你以爲這篇文章的內容對你有幫助,可否幫個忙關注一下筆者的公衆號[前端之境],每週都會努力原創一些前端技術乾貨,關注公衆號後能夠邀你加入前端技術交流羣,咱們能夠一塊兒互相交流,共同進步。
文章已同步更新至Github博客,若覺文章尚可,歡迎前往star!
你的一個點贊,值得讓我付出更多的努力!
逆境中成長,只有不斷地學習,才能成爲更好的本身,與君共勉!