「硬核JS」你真的瞭解垃圾回收機制嗎

聲明:本文爲掘金首發簽約文章,未經受權禁止轉載。前端

寫在前面

咱們知道垃圾回收機制是引擎來作的,JS引擎有不少種(各個瀏覽器都不一樣),其垃圾回收機制在一些細節及優化上略有不一樣,本文咱們以一些通用的回收算法做爲切入,再由 V8 引擎發展至今對該機制的優化爲例(爲何以 V8 爲例?由於它市場佔有率大 😄 ),一步一步深刻來助咱們瞭解垃圾回收機制,由於只有真正瞭解垃圾回收機制,後面才能理解內存泄漏的問題以及手動預防和優化程序員

JavaScript 是門魅力無限的語言,關於它的 GC(垃圾回收)方面,你瞭解多少呢?想來大部分人是由於面試纔去看一些面試題從而瞭解的垃圾回收,那在正式開始以前,給你們列幾個小問題,你們能夠先想一下答案,帶着問題及答案再去看文章,最後讀完此文若是你的答案能夠優化,即有收穫面試

什麼是垃圾回收機制?算法

垃圾是怎樣產生的?數組

爲何要進行垃圾回收?瀏覽器

垃圾回收是怎樣進行的?markdown

V8 引擎對垃圾回收進行了哪些優化?併發

固然,咱們可不只僅是爲了面試,其目的是一次性完全搞懂 GC!假如你對其中某塊內容不太理解,不要着急,先讀完整篇文章瞭解內容再回過頭來仔細看一遍就會清晰不少,乾貨滿滿,先贊後看哦函數

GC是什麼

GCGarbage Collection ,程序工做過程當中會產生不少 垃圾,這些垃圾是程序不用的內存或者是以前用過了,之後不會再用的內存空間,而 GC 就是負責回收垃圾的,由於他工做在引擎內部,因此對於咱們前端來講,GC 過程是相對比較無感的,這一套引擎執行而對咱們又相對無感的操做也就是常說的 垃圾回收機制性能

固然也不是全部語言都有 GC,通常的高級語言裏面會自帶 GC,好比 Java、Python、JavaScript 等,也有無 GC 的語言,好比 C、C++ 等,那這種就須要咱們程序員手動管理內存了,相對比較麻煩

垃圾產生&爲什麼回收

咱們知道寫代碼時建立一個基本類型、對象、函數……都是須要佔用內存的,可是咱們並不關注這些,由於這是引擎爲咱們分配的,咱們不須要顯式手動的去分配內存

可是,你有沒有想過,當咱們再也不須要某個東西時會發生什麼?JavaScript 引擎又是如何發現並清理它的呢?

咱們舉個簡單的例子

let test = {
  name: "isboyjc"
};
test = [1,2,3,4,5]
複製代碼

如上所示,咱們假設它是一個完整的程序代碼

咱們知道 JavaScript 的引用數據類型是保存在堆內存中的,而後在棧內存中保存一個對堆內存中實際對象的引用,因此,JavaScript 中對引用數據類型的操做都是操做對象的引用而不是實際的對象。能夠簡單理解爲,棧內存中保存了一個地址,這個地址和堆內存中的實際值是相關的

那上面代碼首先咱們聲明瞭一個變量 test,它引用了對象 {name: 'isboyjc'},接着咱們把這個變量從新賦值了一個數組對象,也就變成了該變量引用了一個數組,那麼以前的對象引用關係就沒有了,以下圖

沒有了引用關係,也就是無用的對象,這個時候假如任由它擱置,一個兩個還好,多了的話內存也會受不了,因此就須要被清理(回收)

用官方一點的話說,程序的運行須要內存,只要程序提出要求,操做系統或者運行時就必須提供內存,那麼對於持續運行的服務進程,必需要及時釋放內存,不然,內存佔用愈來愈高,輕則影響系統性能,重則就會致使進程崩潰

垃圾回收策略

在 JavaScript 內存管理中有一個概念叫作 可達性,就是那些以某種方式可訪問或者說可用的值,它們被保證存儲在內存中,反之不可訪問則需回收

至於如何回收,其實就是怎樣發現這些不可達的對象(垃圾)它並給予清理的問題, JavaScript 垃圾回收機制的原理說白了也就是按期找出那些再也不用到的內存(變量),而後釋放其內存

你可能還會好奇爲何不是實時的找出無用內存並釋放呢?其實很簡單,實時開銷太大了

咱們均可以 Get 到這之中的重點,那就是怎樣找出所謂的垃圾?

這個流程就涉及到了一些算法策略,有不少種方式,咱們簡單介紹兩個最多見的

  • 標記清除算法
  • 引用計數算法

標記清除算法

策略

標記清除(Mark-Sweep),目前在 JavaScript引擎 裏這種算法是最經常使用的,到目前爲止的大多數瀏覽器的 JavaScript引擎 都在採用標記清除算法,只是各大瀏覽器廠商還對此算法進行了優化加工,且不一樣瀏覽器的 JavaScript引擎 在運行垃圾回收的頻率上有所差別

就像它的名字同樣,此算法分爲 標記清除 兩個階段,標記階段即爲全部活動對象作上標記,清除階段則把沒有標記(也就是非活動對象)銷燬

你可能會疑惑怎麼給變量加標記?其實有不少種辦法,好比當變量進入執行環境時,反轉某一位(經過一個二進制字符來表示標記),又或者能夠維護進入環境變量和離開環境變量這樣兩個列表,能夠自由的把變量從一個列表轉移到另外一個列表,當前還有不少其餘辦法。其實,怎樣標記對咱們來講並不重要,重要的是其策略

引擎在執行 GC(使用標記清除算法)時,須要從出發點去遍歷內存中全部的對象去打標記,而這個出發點有不少,咱們稱之爲一組 對象,而所謂的根對象,其實在瀏覽器環境中包括又不止於 全局Window對象文檔DOM樹

整個標記清除算法大體過程就像下面這樣

  • 垃圾收集器在運行時會給內存中的全部變量都加上一個標記,假設內存中全部對象都是垃圾,全標記爲0
  • 而後從各個根對象開始遍歷,把不是垃圾的節點改爲1
  • 清理全部標記爲0的垃圾,銷燬並回收它們所佔用的內存空間
  • 最後,把全部內存中對象標記修改成0,等待下一輪垃圾回收

優勢

標記清除算法的優勢只有一個,那就是實現比較簡單,打標記也無非打與不打兩種狀況,這使得一位二進制位(0和1)就能夠爲其標記,很是簡單

缺點

標記清除算法有一個很大的缺點,就是在清除以後,剩餘的對象內存位置是不變的,也會致使空閒內存空間是不連續的,出現了 內存碎片(以下圖),而且因爲剩餘空閒內存不是一整塊,它是由不一樣大小內存組成的內存列表,這就牽扯出了內存分配的問題

假設咱們新建對象分配內存時須要大小爲 size,因爲空閒內存是間斷的、不連續的,則須要對空閒內存列表進行一次單向遍歷找出大於等於 size 的塊才能爲其分配(以下圖)

那如何找到合適的塊呢?咱們能夠採起下面三種分配策略

  • First-fit,找到大於等於 size 的塊當即返回

  • Best-fit,遍歷整個空閒列表,返回大於等於 size 的最小分塊

  • Worst-fit,遍歷整個空閒列表,找到最大的分塊,而後切成兩部分,一部分 size 大小,並將該部分返回

這三種策略裏面 Worst-fit 的空間利用率看起來是最合理,但實際上切分以後會形成更多的小塊,造成內存碎片,因此不推薦使用,對於 First-fitBest-fit 來講,考慮到分配的速度和效率 First-fit 是更爲明智的選擇

綜上所述,標記清除算法或者說策略就有兩個很明顯的缺點

  • 內存碎片化,空閒內存塊是不連續的,容易出現不少空閒內存塊,還可能會出現分配所需內存過大的對象時找不到合適的塊
  • 分配速度慢,由於即使是使用 First-fit 策略,其操做還是一個 O(n) 的操做,最壞狀況是每次都要遍歷到最後,同時由於碎片化,大對象的分配效率會更慢

PS:標記清除算法的缺點補充

歸根結底,標記清除算法的缺點在於清除以後剩餘的對象位置不變而致使的空閒內存不連續,因此只要解決這一點,兩個缺點均可以完美解決了

標記整理(Mark-Compact)算法 就能夠有效地解決,它的標記階段和標記清除算法沒有什麼不一樣,只是標記結束後,標記整理算法會將活着的對象(即不須要清理的對象)向內存的一端移動,最後清理掉邊界的內存(以下圖)

引用計數算法

策略

引用計數(Reference Counting),這實際上是早先的一種垃圾回收算法,它把 對象是否再也不須要 簡化定義爲 對象有沒有其餘對象引用到它,若是沒有引用指向該對象(零引用),對象將被垃圾回收機制回收,目前不多使用這種算法了,由於它的問題不少,不過咱們仍是須要了解一下

它的策略是跟蹤記錄每一個變量值被使用的次數

  • 當聲明瞭一個變量而且將一個引用類型賦值給該變量的時候這個值的引用次數就爲 1

  • 若是同一個值又被賦給另外一個變量,那麼引用數加 1

  • 若是該變量的值被其餘的值覆蓋了,則引用次數減 1

  • 當這個值的引用次數變爲 0 的時候,說明沒有變量在使用,這個值無法被訪問了,回收空間,垃圾回收器會在運行的時候清理掉引用次數爲 0 的值佔用的內存

以下例

let a = new Object() 	// 此對象的引用計數爲 1(a引用)
let b = a 		// 此對象的引用計數是 2(a,b引用)
a = null  		// 此對象的引用計數爲 1(b引用)
b = null 	 	// 此對象的引用計數爲 0(無引用)
...			// GC 回收此對象
複製代碼

這種方式是否是很簡單?確實很簡單,不過在引用計數這種算法出現沒多久,就遇到了一個很嚴重的問題——循環引用,即對象 A 有一個指針指向對象 B,而對象 B 也引用了對象 A ,以下面這個例子

function test(){
  let A = new Object()
  let B = new Object()
  
  A.b = B
  B.a = A
}
複製代碼

如上所示,對象 A 和 B 經過各自的屬性相互引用着,按照上文的引用計數策略,它們的引用數量都是 2,可是,在函數 test 執行完成以後,對象 A 和 B 是要被清理的,但使用引用計數則不會被清理,由於它們的引用數量不會變成 0,假如此函數在程序中被屢次調用,那麼就會形成大量的內存不會被釋放

咱們再用標記清除的角度看一下,當函數結束後,兩個對象都不在做用域中,A 和 B 都會被看成非活動對象來清除掉,相比之下,引用計數則不會釋放,也就會形成大量無用內存佔用,這也是後來放棄引用計數,使用標記清除的緣由之一

在 IE8 以及更早版本的 IE 中,BOMDOM 對象並不是是原生 JavaScript 對象,它是由 C++ 實現的 組件對象模型對象(COM,Component Object Model),而 COM 對象使用 引用計數算法來實現垃圾回收,因此即便瀏覽器使用的是標記清除算法,只要涉及到 COM 對象的循環引用,就仍是沒法被回收掉,就好比兩個互相引用的 DOM 對象等等,而想要解決循環引用,須要將引用地址置爲 null 來切斷變量與以前引用值的關係,以下

// COM對象
let ele = document.getElementById("xxx")
let obj = new Object()

// 形成循環引用
obj.ele = ele
ele.obj = obj

// 切斷引用關係
obj.ele = null
ele.obj = null
複製代碼

不過在 IE9 及之後的 BOMDOM 對象都改爲了 JavaScript 對象,也就避免了上面的問題

此處參考 JavaScript高級程序設計 第四版 4.3.2 小節

優勢

引用計數算法的優勢咱們對比標記清除來看就會清晰不少,首先引用計數在引用值爲 0 時,也就是在變成垃圾的那一刻就會被回收,因此它能夠當即回收垃圾

而標記清除算法須要每隔一段時間進行一次,那在應用程序(JS腳本)運行過程當中線程就必需要暫停去執行一段時間的 GC,另外,標記清除算法須要遍歷堆裏的活動以及非活動對象來清除,而引用計數則只須要在引用時計數就能夠了

缺點

引用計數的缺點想必你們也都很明朗了,首先它須要一個計數器,而此計數器須要佔很大的位置,由於咱們也不知道被引用數量的上限,還有就是沒法解決循環引用沒法回收的問題,這也是最嚴重的

V8對GC的優化

咱們在上面也說過,如今大多數瀏覽器都是基於標記清除算法,V8 亦是,固然 V8 確定也對其進行了一些優化加工處理,那接下來咱們主要就來看 V8 中對垃圾回收機制的優化

分代式垃圾回收

試想一下,咱們上面所說的垃圾清理算法在每次垃圾回收時都要檢查內存中全部的對象,這樣的話對於一些大、老、存活時間長的對象來講同新、小、存活時間短的對象一個頻率的檢查很很差,由於前者須要時間長而且不須要頻繁進行清理,後者剛好相反,怎麼優化這點呢???分代式就來了

新老生代

V8 的垃圾回收策略主要基於分代式垃圾回收機制,V8 中將堆內存分爲新生代和老生代兩區域,採用不一樣的垃圾回收器也就是不一樣的策略管理垃圾回收

新生代的對象爲存活時間較短的對象,簡單來講就是新產生的對象,一般只支持 1~8M 的容量,而老生代的對象爲存活事件較長或常駐內存的對象,簡單來講就是經歷過新生代垃圾回收後還存活下來的對象,容量一般比較大

V8 整個堆內存的大小就等於新生代加上老生代的內存(以下圖)

對於新老兩塊內存區域的垃圾回收,V8 採用了兩個垃圾回收器來管控,咱們暫且將管理新生代的垃圾回收器叫作新生代垃圾回收器,一樣的,咱們稱管理老生代的垃圾回收器叫作老生代垃圾回收器好了

新生代垃圾回收

新生代對象是經過一個名爲 Scavenge 的算法進行垃圾回收,在 Scavenge算法 的具體實現中,主要採用了一種複製式的方法即 Cheney算法 ,咱們細細道來

Cheney算法 中將堆內存一分爲二,一個是處於使用狀態的空間咱們暫且稱之爲 使用區,一個是處於閒置狀態的空間咱們稱之爲 空閒區,以下圖所示

新加入的對象都會存放到使用區,當使用區快被寫滿時,就須要執行一次垃圾清理操做

當開始進行垃圾回收時,新生代垃圾回收器會對使用區中的活動對象作標記,標記完成以後將使用區的活動對象複製進空閒區並進行排序,隨後進入垃圾清理階段,即將非活動對象佔用的空間清理掉。最後進行角色互換,把原來的使用區變成空閒區,把原來的空閒區變成使用區

當一個對象通過屢次複製後依然存活,它將會被認爲是生命週期較長的對象,隨後會被移動到老生代中,採用老生代的垃圾回收策略進行管理

另外還有一種狀況,若是複製一個對象到空閒區時,空閒區空間佔用超過了 25%,那麼這個對象會被直接晉升到老生代空間中,設置爲 25% 的比例的緣由是,當完成 Scavenge 回收後,空閒區將翻轉成使用區,繼續進行對象內存的分配,若佔比過大,將會影響後續內存分配

老生代垃圾回收

相比於新生代,老生代的垃圾回收就比較容易理解了,上面咱們說過,對於大多數佔用空間大、存活時間長的對象會被分配到老生代裏,由於老生代中的對象一般比較大,若是再如新生代通常分區而後複製來複制去就會很是耗時,從而致使回收執行效率不高,因此老生代垃圾回收器來管理其垃圾回收執行,它的整個流程就採用的就是上文所說的標記清除算法了

首先是標記階段,從一組根元素開始,遞歸遍歷這組根元素,遍歷過程當中能到達的元素稱爲活動對象,沒有到達的元素就能夠判斷爲非活動對象

清除階段老生代垃圾回收器會直接將非活動對象,也就是數據清理掉

前面咱們也提過,標記清除算法在清除後會產生大量不連續的內存碎片,過多的碎片會致使大對象沒法分配到足夠的連續內存,而 V8 中就採用了咱們上文中說的標記整理算法來解決這一問題來優化空間

爲何須要分代式?

正如小標題,爲何須要分代式?這個機制有什麼優勢又解決了什麼問題呢?

其實,它並不能說是解決了什麼問題,能夠說是一個優化點吧

分代式機制把一些新、小、存活時間短的對象做爲新生代,採用一小塊內存頻率較高的快速清理,而一些大、老、存活時間長的對象做爲老生代,使其不多接受檢查,新老生代的回收機制及頻率是不一樣的,能夠說此機制的出現很大程度提升了垃圾回收機制的效率

並行回收(Parallel)

在介紹並行以前,咱們先要了解一個概念 全停頓(Stop-The-World),咱們都知道 JavaScript 是一門單線程的語言,它是運行在主線程上的,那在進行垃圾回收時就會阻塞 JavaScript 腳本的執行,需等待垃圾回收完畢後再恢復腳本執行,咱們把這種行爲叫作 全停頓

好比一次 GC 須要 60ms ,那咱們的應用邏輯就得暫停 60ms ,假如一次 GC 的時間過長,對用戶來講就可能形成頁面卡頓等問題

既然存在執行一次 GC 比較耗時的狀況,考慮到一我的蓋房子難,那兩我的、十我的...呢?切換到程序這邊,那咱們可不能夠引入多個輔助線程來同時處理,這樣是否是就會加速垃圾回收的執行速度呢?所以 V8 團隊引入了並行回收機制

所謂並行,也就是同時的意思,它指的是垃圾回收器在主線程上執行的過程當中,開啓多個輔助線程,同時執行一樣的回收工做

簡單來講,使用並行回收,假如原本是主線程一我的幹活,它一我的須要 3 秒,如今叫上了 2 個輔助線程和主線程一塊幹活,那三我的一塊幹一我的幹 1 秒就完事了,可是因爲多人協同辦公,因此須要加上一部分多人協同(同步開銷)的時間咱們算 0.5 秒好了,也就是說,採用並行策略後,原本要 3 秒的活如今 1.5 秒就能夠幹完了

不過雖然 1.5 秒就能夠幹完了,時間也大大縮小了,可是這 1.5 秒內,主線程仍是須要讓出來的,也正是由於主線程仍是須要讓出來,這個過程內存是靜態的,不須要考慮內存中對象的引用關係改變,只須要考慮協同,實現起來也很簡單

新生代對象空間就採用並行策略,在執行垃圾回收的過程當中,會啓動了多個線程來負責新生代中的垃圾清理操做,這些線程同時將對象空間中的數據移動到空閒區域,這個過程當中因爲數據地址會發生改變,因此還須要同步更新引用這些對象的指針,此即並行回收

增量標記與懶性清理

咱們上面所說的並行策略雖然能夠增長垃圾回收的效率,對於新生代垃圾回收器可以有很好的優化,可是其實它仍是一種全停頓式的垃圾回收方式,對於老生代來講,它的內部存放的都是一些比較大的對象,對於這些大的對象 GC 時哪怕咱們使用並行策略依然可能會消耗大量時間

因此爲了減小全停頓的時間,在 2011 年,V8 對老生代的標記進行了優化,從全停頓標記切換到增量標記

什麼是增量

增量就是將一次 GC 標記的過程,分紅了不少小步,每執行完一小步就讓應用邏輯執行一下子,這樣交替屢次後完成一輪 GC 標記(以下圖)

試想一下,將一次完整的 GC 標記分次執行,那在每一小次 GC 標記執行完以後如何暫停下來去執行任務程序,然後又怎麼恢復呢?那假如咱們在一次完整的 GC 標記分塊暫停後,執行任務程序時內存中標記好的對象引用關係被修改了又怎麼辦呢?

能夠看出增量的實現要比並行復雜一點,V8 對這兩個問題對應的解決方案分別是三色標記法與寫屏障

三色標記法(暫停與恢復)

咱們知道老生代是採用標記清理算法,而上文的標記清理中咱們說過,也就是在沒有采用增量算法以前,單純使用黑色和白色來標記數據就能夠了,其標記流程即在執行一次完整的 GC 標記前,垃圾回收器會將全部的數據置爲白色,而後垃圾回收器在會從一組跟對象出發,將全部能訪問到的數據標記爲黑色,遍歷結束以後,標記爲黑色的數據對象就是活動對象,剩餘的白色數據對象也就是待清理的垃圾對象

若是採用非黑即白的標記策略,那在垃圾回收器執行了一段增量回收後,暫停後啓用主線程去執行了應用程序中的一段 JavaScript 代碼,隨後當垃圾回收器再次被啓動,這時候內存中黑白色都有,咱們沒法得知下一步走到哪裏了

爲了解決這個問題,V8 團隊採用了一種特殊方式: 三色標記法

三色標記法即便用每一個對象的兩個標記位和一個標記工做表來實現標記,兩個標記位編碼三種顏色:白、灰、黑

  • 白色指的是未被標記的對象
  • 灰色指自身被標記,成員變量(該對象的引用對象)未被標記
  • 黑色指自身和成員變量皆被標記

如上圖所示,咱們用最簡單的表達方式來解釋這一過程,最初全部的對象都是白色,意味着回收器沒有標記它們,從一組根對象開始,先將這組根對象標記爲灰色並推入到標記工做表中,當回收器從標記工做表中彈出對象並訪問它的引用對象時,將其自身由灰色轉變成黑色,並將自身的下一個引用對象轉爲灰色

就這樣一直往下走,直到沒有可標記灰色的對象時,也就是無可達(無引用到)的對象了,那麼剩下的全部白色對象都是沒法到達的,即等待回收(如上圖中的 C、E 將要等待回收)

採用三色標記法後咱們在恢復執行時就好辦多了,能夠直接經過當前內存中有沒有灰色節點來判斷整個標記是否完成,如沒有灰色節點,直接進入清理階段,如還有灰色標記,恢復時直接從灰色的節點開始繼續執行就能夠

三色標記法的 mark 操做能夠漸進執行的而不需每次都掃描整個內存空間,能夠很好的配合增量回收進行暫停恢復的一些操做,從而減小 全停頓 的時間

寫屏障(增量中修改引用)

一次完整的 GC 標記分塊暫停後,執行任務程序時內存中標記好的對象引用關係被修改了,增量中修改引用,可能不太好理解,咱們舉個例子(如圖)

假如咱們有 A、B、C 三個對象依次引用,在第一次增量分段中所有標記爲黑色(活動對象),然後暫停開始執行應用程序也就是 JavaScript 腳本,在腳本中咱們將對象 B 的指向由對象 C 改成了對象 D ,接着恢復執行下一次增量分段

這時其實對象 C 已經無引用關係了,可是目前它是黑色(表明活動對象)此一整輪 GC 是不會清理 C 的,不過咱們能夠不考慮這個,由於就算此輪不清理等下一輪 GC 也會清理,這對咱們程序運行並無太大影響

咱們再看新的對象 D 是初始的白色,按照咱們上面所說,已經沒有灰色對象了,也就是所有標記完畢接下來要進行清理了,新修改的白色對象 D 將在次輪 GC 的清理階段被回收,還有引用關係就被回收,後面咱們程序裏可能還會用到對象 D 呢,這確定是不對的

爲了解決這個問題,V8 增量回收使用 寫屏障 (Write-barrier) 機制,即一旦有黑色對象引用白色對象,該機制會強制將引用的白色對象改成灰色,從而保證下一次增量 GC 標記階段能夠正確標記,這個機制也被稱做 強三色不變性

那在咱們上圖的例子中,將對象 B 的指向由對象 C 改成對象 D 後,白色對象 D 會被強制改成灰色

懶性清理

增量標記其實只是對活動對象和非活動對象進行標記,對於真正的清理釋放內存 V8 採用的是惰性清理(Lazy Sweeping)

增量標記完成後,惰性清理就開始了。當增量標記完成後,假如當前的可用內存足以讓咱們快速的執行代碼,其實咱們是不必當即清理內存的,能夠將清理過程稍微延遲一下,讓 JavaScript 腳本代碼先執行,也無需一次性清理完全部非活動對象內存,能夠按需逐一進行清理直到全部的非活動對象內存都清理完畢,後面再接着執行增量標記

增量標記與惰性清理的優缺?

增量標記與惰性清理的出現,使得主線程的停頓時間大大減小了,讓用戶與瀏覽器交互的過程變得更加流暢。可是因爲每一個小的增量標記之間執行了 JavaScript 代碼,堆中的對象指針可能發生了變化,須要使用寫屏障技術來記錄這些引用關係的變化,因此增量標記缺點也很明顯:

首先是並無減小主線程的總暫停的時間,甚至會略微增長,其次因爲寫屏障機制的成本,增量標記可能會下降應用程序的吞吐量(吞吐量是啥總不用說了吧)

併發回收(Concurrent)

前面咱們說並行回收依然會阻塞主線程,增量標記一樣有增長了總暫停時間、下降應用程序吞吐量兩個缺點,那麼怎麼才能在不阻塞主線程的狀況下執行垃圾回收而且與增量相比更高效呢?

這就要說到併發回收了,它指的是主線程在執行 JavaScript 的過程當中,輔助線程可以在後臺完成執行垃圾回收的操做,輔助線程在執行垃圾回收的時候,主線程也能夠自由執行而不會被掛起(以下圖)

輔助線程在執行垃圾回收的時候,主線程也能夠自由執行而不會被掛起,這是併發的優勢,但一樣也是併發回收實現的難點,由於它須要考慮主線程在執行 JavaScript 時,堆中的對象引用關係隨時都有可能發生變化,這時輔助線程以前作的一些標記或者正在進行的標記就會要有所改變,因此它須要額外實現一些讀寫鎖機制來控制這一點,這裏咱們再也不細說

再說V8中GC優化

V8 的垃圾回收策略主要基於分代式垃圾回收機制,這咱們說過,關於新生代垃圾回收器,咱們說使用並行回收能夠很好的增長垃圾回收的效率,那老生代垃圾回收器用的哪一個策略呢?我上面說了並行回收、增量標記與惰性清理、併發回收這幾種回收方式來提升效率、優化體驗,看着一個比一個好,那老生代垃圾回收器到底用的哪一個策略?難道是併發??心裏獨白:」 好像。。貌似。。併發回收效率最高 「

其實,這三種方式各有優缺點,因此在老生代垃圾回收器中這幾種策略都是融合使用的

老生代主要使用併發標記,主線程在開始執行 JavaScript 時,輔助線程也同時執行標記操做(標記操做全都由輔助線程完成)

標記完成以後,再執行並行清理操做(主線程在執行清理操做時,多個輔助線程也同時執行清理操做)

同時,清理的任務會採用增量的方式分批在各個 JavaScript 任務之間執行

最後

那上面就是 V8 引擎爲咱們的垃圾回收所作的一些主要優化了,雖然引擎有優化,但並非說咱們就能夠徹底不用關心垃圾回收這塊了,咱們的代碼中依然要主動避免一些不利於引擎作垃圾回收操做,由於不是全部無用對象內存均可以被回收的,那當再也不用到的內存,沒有及時回收時,咱們叫它 內存泄漏

關於內存泄漏又是另外一個點了,也礙於篇幅就不放在這篇文章了

收工,看也看完了,開頭的問題你有更深層次的答案了嗎?在以前面試時我問過面試者這類問題,大多同窗的回答都僅限於標記清除+引用計數兩個概念,往深處各類缺陷以及優化上挖一挖就說不出了,其實咱們結合 V8 引擎對垃圾回收的優化來回答上面那些問題會更好一些,那麼,評論區碼出本身的理解吧!

另外,有哪些沒有 Get 到的點能夠評論留言,也歡迎指錯勘誤!!!

相關文章
相關標籤/搜索