前端面試查漏補缺--(二) 垃圾回收機制

前言

本系列最開始是爲了本身面試準備的.後來發現整理愈來愈多,差很少有十二萬字符,最後決定仍是分享出來給你們.javascript

爲了分享整理出來,花費了本身大量的時間,起碼是隻本身用的三倍時間.若是喜歡的話,歡迎收藏,關注我!謝謝!前端

文章連接

合集篇:

前端面試查漏補缺--Index篇(12萬字符合集) 包含目前已寫好的系列其餘十幾篇文章.後續新增值文章不會再在每篇添加連接,強烈建議議點贊,關注合集篇!!!!,謝謝!~vue

後續更新計劃

後續還會繼續添加設計模式,前端工程化,項目流程,部署,閉環,vue常考知識點 等內容.若是以爲內容不錯的話歡迎收藏,關注我!謝謝!java

求一分內推

目前本人也在準備跳槽,但願各位大佬和HR小姐姐能夠內推一份靠譜的武漢 前端崗位!郵箱:bupabuku@foxmail.com.謝謝啦!~node

垃圾回收機制

JavaScript 具備自動垃圾收集機制(GC:GarbageCollecation),也就是說,執行環境會負責管理代碼執行過程當中使用的內存。開發人員不用再關心內存使用問題,所需內存的分配以及無用內存的回收徹底實現了自動管理。git

內存生命週期

JS環境中分配的內存通常有以下生命週期:github

  1. 內存分配:當咱們申明變量、函數、對象,並執行的時候,系統會自動爲他們分配內存
  2. 內存使用:即讀寫內存,也就是使用變量、函數等
  3. 內存回收:使用完畢,由垃圾回收機制自動回收再也不使用的內存

垃圾回收機制策略

標記清除算法

JavaScript 中最經常使用的垃圾收集方式是標記清除(mark-and-sweep)。面試

這個算法把「對象是否再也不須要」簡化定義爲「對象是否能夠得到」。算法

該算法假定設置一個叫作根(root)的對象(在Javascript裏,根是全局對象)。垃圾回收器將按期從根開始(在JS中就是全局對象)掃描內存中的對象。凡是能從根部到達的對象,都是還須要使用的。那些沒法由根部出發觸及到的對象被標記爲再也不使用,稍後進行回收。設計模式

此算法能夠分爲兩個階段,一個是標記階段(mark),一個是清除階段(sweep)。

  1. 標記階段,垃圾回收器會從根對象開始遍歷。每個能夠從根對象訪問到的對象都會被添加一個標識,因而這個對象就被標識爲可到達對象。
  2. 清除階段,垃圾回收器會對堆內存從頭至尾進行線性遍歷,若是發現有對象沒有被標識爲可到達對象,那麼就將此對象佔用的內存回收,而且將原來標記爲可到達對象的標識清除,以便進行下一次垃圾回收操做。

在標記階段,從根對象1能夠訪問到B,從B又能夠訪問到E,那麼B和E都是可到達對象,一樣的道理,F、G、J和K都是可到達對象。

在回收階段,全部未標記爲可到達的對象都會被垃圾回收器回收。

什麼時候開始垃圾回收?
一般來講,在使用標記清除算法時,未引用對象並不會被當即回收。取而代之的作法是,垃圾對象將一直累計到內存耗盡爲止。當內存耗盡時,程序將會被掛起,垃圾回收開始執行。

補充: 從2012年起,全部現代瀏覽器都使用了標記-清除垃圾回收算法。全部對JavaScript垃圾回收算法的改進都是基於標記-清除算法的改進,並無改進標記-清除算法自己和它對「對象是否再也不須要」的簡化定義。

標記清除算法缺陷

  • 那些沒法從根對象查詢到的對象都將被清除
  • 垃圾收集後有可能會形成大量的內存碎片,像上面的圖片所示,垃圾收集後內存中存在三個內存碎片,假設一個方格表明1個單位的內存,若是有一個對象須要佔用3個內存單位的話,那麼就會致使Mutator一直處於暫停狀態,而Collector一直在嘗試進行垃圾收集,直到Out of Memory。

引用計數算法

這是最初級的垃圾收集算法.如今已經沒有瀏覽器會用這種算法.

此算法把「對象是否再也不須要」簡化定義爲「對象有沒有其餘對象引用到它」。若是沒有引用指向該對象(零引用),對象將被垃圾回收機制回收。

引用計數的含義是跟蹤記錄每一個值被引用的次數。當聲明瞭一個變量並將一個引用類型值賦給該變量時,則這個值的引用次數就是1。若是同一個值又被賦給另外一個變量,則該值的引用次數加1。相反,若是包含對這個值引用的變量又取得了另一個值,則這個值的引用次數減1。當這個值的引用次數變成0時,則說明沒有辦法再訪問這個值了,於是就能夠將其佔用的內存空間回收回來。這樣,當垃圾收集器下次再運行時,它就會釋放那些引用次數爲零的值所佔用的內存。

引用計數缺陷

該算法有個限制:沒法處理循環引用。若是兩個對象被建立,並互相引用,造成了一個循環。它們被調用以後會離開函數做用域,因此它們已經沒有用了,能夠被回收了。然而,引用計數算法考慮到它們互相都有至少一次引用,因此它們不會被回收。

Chrome V8 垃圾回收算法

Chrome 瀏覽器所使用的 V8 引擎就是採用的分代回收策略。這個和 Java 回收策略思想是一致的。目的是經過區分「臨時」與「持久」對象;多回收「臨時對象區」(新生代younggeneration),少回收「持久對象區」(老生代 tenured generation),減小每次需遍歷的對象,從而減小每次GC的耗時。

V8的內存限制

在node中javascript能使用的內存是有限制的.

  1. 64位系統下約爲1.4GB。
  2. 32位系統下約爲0.7GB。

對應到分代內存中,默認狀況下。

  1. 32位系統新生代內存大小爲16MB,老生代內存大小爲700MB。
  2. 64位系統下,新生代內存大小爲32MB,老生代內存大小爲1.4GB。

新生代平均分紅兩塊相等的內存空間,叫作semispace,每塊內存大小8MB(32位)或16MB(64位)。

這個限制在node啓動的時候能夠經過傳遞--max-old-space-size 和 --max-new-space-size來調整,如:

node --max-old-space-size=1700 app.js //單位爲MB
node --max-new-space-size=1024 app.js //單位爲kb
複製代碼

上述參數在V8初始化時生效,一旦生效就不能再動態改變。

內存限制的緣由:

至於V8爲什麼要限制堆的大小,表層緣由:V8最初爲瀏覽器而設計,不太可能遇到用大量內存的場景。深層緣由:V8的垃圾回收機制的限制。官方說法,以1.5GB的垃圾回收堆內存爲例,V8作一次小的垃圾回收須要50毫秒以上,作一次非增量式的垃圾回收甚至要1秒以上。這是垃圾回收中引發JS線程暫停執行的時間,在這樣時間花銷下,應用的性能和響應能力都會直線降低。

V8的分代回收(Generation GC)

V8垃圾回收策略主要基於分代式垃圾回收機制。現代的垃圾回收算法中按對象的存活時間將內存的垃圾回收進行不一樣的分代,而後分別對不一樣分代的內存施以更高效的算法。

V8的內存分代:

在V8中,主要將內存分爲新生代和老生代,新生代內存 存儲的爲存活時間較短的對象老生代內存 存儲的爲存活時間較長或常駐內存的對象,以下圖:

V8堆的總體大小就是新生代所用內存空間加上老生代的內存空間。

V8新生代算法(Scavenge):

在分代基礎上,新生代中的對象主要經過Scavenge算法進行垃圾回收。在Scavenge的具體實現中,主要採用了Cheney算法

Cheney算法是一種採用複製的方式實現的垃圾回收算法。它將堆內存一分爲二,每一部分空間稱爲semispace。在這兩個semispace空間中,只有一個處於使用中,另外一個處於閒置狀態。處於使用狀態的semispace空間稱爲From空間,處於閒置狀態的空間稱爲To空間。

當咱們分配對象時,先是在From空間中進行分配。當開始進行垃圾回收時,會檢查From空間中的存活對象,這些存活對象將被複制到To空間中,而(From空間內的)非存活對象佔用的空間將會被釋放。完成複製後,From空間和To空間的角色發生對換(即之前的From空間釋放後變爲To;To空間在複製存活的對象後,變爲From空間)。簡而言之,在垃圾回收過程當中,就是經過將存活對象在兩個semispace空間之間進行復制。

Scavenge的缺點:
只能使用堆內存中的一半,這是由劃分空間和複製機制所決定的。

Scavenge的優勢:
Scavenge因爲只複製存活的對象,而且對於生命週期短的場景存活對象只佔少部分,因此它在時間效率上有優異的表現。 Scavenge是典型的犧牲空間換取時間的算法, 因此沒法大規模地應用到全部的垃圾回收中。但能夠發現,Scavenge很是適合應用在新生代中,由於新生代中對象的生命週期較短,偏偏適合這個算法。

晉升:
實際使用的堆內存是新生代的兩個semispace空間大小和老生代所用內存大小之和。當一個對象通過屢次複製依然存活時,它將會被認爲是生命週期較長的對象。這種較長生命週期的對象隨後會被移動到老生代中,採用新的算法進行管理。對象重新生代中移動到老生代中的過程稱爲晉升。

在單純的Scavenge過程當中,From空間中的存活對象會被複制到To空間中去,而後對From空間和To空間進行角色對換(又稱翻轉)。但在分代式垃圾回收前提下,From空間中的存活對象在複製到To空間以前須要進行檢查。在必定條件下,須要將存活週期長的對象移動到老生代中,也就是完成對象晉升。

晉升條件:
對象晉升的條件主要有兩個,一個是對象是否經歷過Scavenge回收,一個是To空間的內存佔用比超過25%限制。

設置25%這個限制值的緣由:
當此次Scavenge回收完成後,這個To空間將變成From空間,接下來的內存分配將在這個空間中進行。若是佔比太高,會影響後續的內存分配。 對象晉升後,將會在老生代空間中做爲存活週期較長的對象來對待,接受新的回收算法處理。

是否經歷過Scavenge回收

To空間的使用應超過25%時

V8老生代算法(Mark-Sweep && Mark-Compact):

對於老生代中的對象,因爲存活對象佔較大比重,再採用Scavenge的方式會有兩個問題:一個是存活對象較多,複製存活對象的效率將會很低;另外一個問題依然是浪費一半空間的問題。爲此,V8在老生代中主要採用Mark-Sweep和Mark-Compact相結合的方式進行垃圾回收。

Mark-Sweep:
Mark-Sweep是標記清除的意思,它分爲標記和清除兩個階段。與Scavenge相比,Mark-Sweep並不將內存空間劃分爲兩半,因此不存在浪費一半空間的行爲。與Scavenge複製活着的對象不一樣,Mark-Sweep在標記階段遍歷堆中全部對象,並標記活着的對象,在隨後的清除階段中,只清除沒有被標記的對象。 能夠看出,Scavenge中只複製活着的對象,而Mark-Sweep只清理死亡對象。 活對象在新生代中只佔較小部分,死對象在老生代中只佔較小部分,這是兩種回收方式能高效處理的緣由。

下圖爲Mark-Sweep在老生代空間中標記的示意圖,黑色部分標記爲死亡對象

Mark-Sweep最大的問題:
在進行一次標記清除回收後,內存空間會出現不連續的狀態。這種內存碎片會對後續的內存分配形成問題,由於極可能出現須要分配一個大對象的狀況,這時全部的碎片空間都沒法完成這次分配,就會提早觸發垃圾回收,而此次回收是沒必要要的。(注意理解這句話,不要把內存想象成液體.而是固體,就像一個個散亂排列的麻將,須要進行排序處理--即後面要講的 Mark-Compact)

Mark-Compact:
爲了解決Mark-Sweep的內存碎片問題,Mark-Compact被提出來。Mark-Compact是標記整理的意思,是在Mark-Sweep的基礎上演變而來的。它們的差異在於對象在標記爲死亡後,在整理的過程當中,將活着的對象往一端移動,移動完成後,直接清理掉邊界外的內存。 下圖爲Mark-Compact完成標記並移動存活對象後的示意圖,白色格子爲存活對象,深色格子爲死亡對象,淺色格子爲存活對象移動後留下的空洞。

完成移動後,就能夠直接清除最右邊的存活對象後面的內存區域完成回收。

Mark-Sweep、Mark-Compact、Scavenge三種主要垃圾回收算法的簡單對比

回收算法 Mark-Sweep Mark-Compact Scavenge
速度 中等 最慢 最快
空間開銷 少(有碎片) 少(無碎片) 雙倍空間(無碎片)
是否移動對象

從表格上看,Mark-Sweep和Mark-Compact之間,因爲Mark-Compact須要移動對象,因此它的執行速度不可能很快,因此在取捨上,V8主要使用Mark-Sweep,在空間不足以對重新生代中晉升過來的對象進行分配時才使用Mark-Compact。

增量式標記回收(Incremental Marking):

  • 爲了不出現js應用邏輯與垃圾回收器看到的不一致的狀況,垃圾回收的3種基本算法都須要將應用邏輯暫停下來,待執行完垃圾回收後再恢復執行應用邏輯,這種行爲被稱爲「全停頓」(stop-the-world)。在V8的分代式垃圾回收中,一次小垃圾回收只收集新生代,因爲新生代默認配置得較小,且其中存活對象一般較少,因此即使它是全停頓的影響也不大。但V8的老生代一般配置得較大,且存活對象較多,全堆垃圾回收(full垃圾回收)的標記、清理、整理等動做形成的停頓就會比較可怕,須要設法改善(PS: 若V8的堆內存爲1.5GB,V8作一次小的垃圾回收須要50ms以上,作一次非增量式的垃圾回收甚至要1秒以上。)。
  • 爲了下降全堆垃圾回收帶來的停頓時間,V8先從標記階段入手,將本來要一口氣停頓完成的動做改成增量標記(incremental marking),也就是拆分爲許多小「步進」,每作完一「步進」就讓js應用邏輯執行一小會,垃圾回收與應用邏輯交替執行直到標記階段完成。
  • V8在通過增量標記的改進後,垃圾回收的最大停頓時間能夠減小到本來的1/6左右。
  • V8後續還引入了延遲清理(lazy sweeping)與增量式整理(incremental compaction),讓清理與整理動做也變成增量式的。同時還計劃引入並行標記與並行清理,進一步利用多核性能下降每次停頓的時間。

減小垃圾和回收對性能的影響:

主要注意如下兩點:

  • 讓垃圾回收儘可能少地進行,尤爲是全堆垃圾回收。這部分咱們基本上幫不了什麼忙,主要靠v8本身的優化機制.
  • 避免內存泄露,讓內存及時獲得釋放. 這部分是咱們須要注意的.具體能夠查看,本系列的內存泄露章節,有超詳細講解.

感謝及參考

相關文章
相關標籤/搜索