垃圾回收:你只知道垃圾回收,不知道垃圾到底咋回收?!

開篇勸退

先告訴閱讀本篇文章的同窗本人水平有限,菜鳥一隻。javascript

  • 分享內容是沒人不知道的javascript垃圾回收機制
  • 若是面試官問你垃圾回收是什麼,你只能用10句話之內就表述完,那麼這篇文章很適合你。
  • 將會具體解釋Node使用的V8引擎是如何高效的使用內存、如何執行垃圾回收以及對各類變量內存的具體操做。

提示:本篇文章不適合熟讀各類源碼的大佬閱讀,會浪費時間java

2009年Raan Dahl選擇了Chrome的V8來做爲Node的javascript引擎,不止是由於它的事件驅動,非阻塞I/O模型等特色,還有很重要的V8的內存高效處理機制,爲Node的崛起和推廣鑑定了基礎。面試

V8的內存限制

咱們先抑後揚,Node不一樣於其餘後端語言,Node在對系統的內存使用中,只能使用到系統的部份內存,好比64位系統只能使用1.4GB,32位系統只能使用0.7GB。隨之到來的問題是Node採用單線程,就致使每一個線程沒法對大的內存對象進行處理,好比將一個2GB的文件讀入內存進行字符串分析處理,即便你有16G的物理內存。算法

V8的對象分配

在javascript中咱們的基本類型存儲在棧中,全部對象都分配給了堆處理。 咱們每賦值一個對象,該對象的內存就會分配在堆中。若是已申請堆所剩內存不足以分配新的對象,將會繼續申請新內存,直到堆的大小超過V8的內存大小限制爲止。 chrome

在這裏插入圖片描述
至於V8的內存限制,起源於V8自己是chrome爲瀏覽器設計而生,而瀏覽器中對於網頁來講,V8控制的內存綽綽有餘。還源於V8設計者對於V8的垃圾回收機制的限制,官方以 1.5GB的垃圾回收堆內存爲例,V8執行一個小的垃圾回收要使用 50毫秒以上,作一次常規非增量式垃圾回收要在1秒以上。

最關鍵的,javascript的垃圾回收會對javascript執行線程造成阻塞,做爲一個開發人員你應該可以清楚時長1秒的進程阻塞,對你的項目性能的影響,故此V8的設計者採用了對堆內存進行限制的策略。後端

V8的內存分代

V8的垃圾回收策略主要基於分代,那麼怎麼分代呢?瀏覽器

在V8中,主要將內存分爲新生代老生代兩類。新生代指的是那些存活時間較短的對象,老生代指的是存活時間較長的或者常駐內存的對象。而新生代加老生代的對象所佔空間大小就是V8的堆的總體大小。 閉包

補充知識點:V8提供了設置新生代和老生代最大內存值的方式,從而能夠調整V8的總體內存限制,使用更多的內存空間。併發

使用--max-old-space-size來調整老生代最大空間和--max-new-space-size來調整新生代最大空間,可是該操 做須要在Node進程啓動時就設置纔有效。函數

V8的主要垃圾回收算法

Scanvenge算法

Scanvenge是一種複製形式的垃圾回收算法,是應用於新生代對象中的一種垃圾回收算法,算法首先將堆內存一分爲二,兩部分空間一半用來分配賦值的對象,叫作From空間,另外一半處於空閒的叫作To空間。

爲何要有一半空間用來閒置呢?這不是讓咱們的可用內存更小了嗎?

當咱們爲堆分配對象時,會將分配對象放到From空間中存儲,在V8的垃圾和回收過程當中,會首先檢查From中存活的對象(什麼是存活的對象,就是指那些還被繼續引用沒有徹底釋放的對象),V8會將From中存活的對象夫婦複製到To空間中,同時清理掉已經被釋放的對象空間。完成該過程From空間和To空間即完成了角色對換,也就是在下一次回收中,以前的From空間變成了To空間,以前的To空間變成了From空間。

這樣咱們來從新定義一下:

用來存放對象的一半是From空間,處於閒置狀態的一半是To空間。

在這裏插入圖片描述
Scanvenge算法明顯的缺點就是隻能使用堆內存的一半,可是隨之帶來的好處就是它在時間效率上的優異的表現,屬於典型的犧牲空間換取時間的算法。 須要強調的是,開頭提到的 Scanvenge算法是應用於新生代對象中的一種垃圾回收算法,由於新生代對象中的生命週期較短的特性,也契合於該算法優先時間考慮的特性。

怎樣算生命週期較長的對象?

當一個對象通過屢次複製依然存活時,它將會被認爲是生命週期較長的對象。這種生命週期較長的對象隨後會被移動到老生代對象中,採用新的算法(Mark-Sweep&Mark-Compact)進行管理,這個過程稱爲晉升。

經過上圖能夠了解到,對象進行垃圾回收是怎樣從From到To之間轉換的,那麼這個晉升的過程在哪兒體現呢?

在默認狀況下,V8對新生代對象進行從From到To空間進行復制時,會先檢查它的內存地址來判斷這個對象是否已經經歷過一次Scanvenge回收。若是已經經歷過,那麼會將該對象從From空間直接複製到老生代空間,若是沒有,纔會將其複製到To空間。

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

以上,咱們講述的就是一個新生代對象如何晉升爲老生代對象的第一個條件「對象是否經歷過Scanvenge回收」,那麼第二個條件也許你會更困惑,超出限制?多少算在限制?怎麼超出?

假設一個對象像剛纔說的沒有經歷過Scanvenge回收,要將它複製到To空間以前,還要再進行一次檢查。檢查To空間是否已經使用了超過25%,若是To空間超過25%,該對象將直接被晉升到老生代空間進行管理。

完整看一下這個流程:

對象晉升後,該對象即成爲老生代中的存活週期較長的對象,因此咱們能夠從新對老生代進行定義:老生代對象爲存活週期較長或常駐內存的對象,或爲新生代對象回收中溢出的對象

至於爲何設置25%的緣由是,當一次Scanvenge回收完成時,To空間變爲From空間,若是新的From空間使用佔比太高,將對接下來的內存分配到這個新的From空間過程存在很大的影響。

Mark-Sweep&Mark-Compact算法

接下來,講一下老生代中的對象使用的回收算法,這種算法(Mark-Sweep)也是咱們常說的垃圾回收中的標記清除算法。

首先,老生代空間不會一分爲二,老生代空間進行垃圾回收時,首先是標記階段。V8會在標記階段遍歷老生代空間中的全部對象,並標記存活的對象(即尚未被徹底釋放的對象),在隨後的清除階段,會將全部未標記的老生代對象所有回收。

再來張圖:

若是你稍微有點強迫症,你就發現這張圖有點問題。Mark-Sweep在執行完清除以後,致使內存空間出現不連續的狀況,就像你的磁盤分析圖同樣。

這樣會帶來的一個問題就是,當你須要分配一個較大的對象時,剩餘的內存由於碎片化的緣由,沒有任何一個內存碎片足以分配給這個大的對象內存空間,就會致使提早觸發垃圾回收,而此次回收是沒必要要的。

因此Mark-Compact算法隨之而生,Mark-Compact比Mark-Sweep增長了一個整理的概念,它的回收執行順序是標記—整理—清除。Mark-Compact所謂的整理概念是指在對象一樣被標記爲存活後,會將活着的對象往一端移動,移動完成後在直接清理掉死亡的對象內存。

不要暈,來張圖,你就能夠的:

在這裏插入圖片描述

兩種差異顯而易見,Mark-Compact算法執行後的內存空間更合理。可是由於Mark-Compact算法須要移動對象,隨之致使的就是它的執行速度沒有Mark-Sweep快。

因此在V8中主要使用Mark-Sweep算法,只有在空間不足以對新生代中晉升過來的對象進行分配時,纔會使用Mark-Compact算法進行回收。

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

Incremental Marking算法

由於垃圾回收會阻塞javascript的運行,故此老生代對象又由於其佔用空間大,存活對象多的特色,對其進行標記,整理,回收的過程引發的阻塞要遠遠比新生代對象回收過程一塊兒的阻塞要嚴重的多,Incremental Marking算法成爲了優化老生代對象耗時的算法選擇。

爲了下降老生代空間垃圾回收帶來的停頓影響,V8 採用了增量標記(incremental marking)的算法。將本來一口氣停頓完成的來及回收過程拆分爲許多小「步進」,每作完一「步進」就讓JavaScript應用邏輯繼續執行一小會兒,垃圾回收與應用邏輯交替執行直到標記階段完成。取得的效果就是,將老生代空間垃圾回收的最大停頓時間能夠減小到本來的1/6左右。

有點暈,不要怕,咱有圖:

在這裏插入圖片描述

V8 後續還引入了延遲清理(lazy sweeping)、增量式整理(incremental compaction)、併發標記 等技術,其實看名字你也能理解大概,能夠自行查閱。

擴展知識

相信上面的東西已經讓大家明白,V8的垃圾回收機制是如何運行的。可是你還須要知道咱們的代碼中是如何觸發、影響垃圾回收的,這就不得不掏出老生常談的做用域和閉包。

做用域

在javascript中做用域有全局做用域和局部做用域,在這裏,咱們着重關注做用域對垃圾回收的影響。

假設一個函數調用產生的做用域:

var foo = function(){
	var local = {}
}
複製代碼

這是一個函數表達式,foo()函數在每次調用時會建立一個做用域,同時也會在該做用域建立一個局部變量local。函數執行結束,該做用域也會隨之銷燬,同時該做用域中聲明的局部變量也會隨做用域銷燬而銷燬。在這個實例中,因爲局部變量引用的對象存活週期較短,將會分配在新生代空間的From中。做用域銷燬後,其中的變量也隨之被釋放,該對象所佔用的空間在下次垃圾回收時將會被清理。

做用域鏈

var foo = function(){
	var local = 100
	var bar = function(){
		console.log(local)
	}
}
複製代碼

在這個實例中,bar()中執行console,在當前函數做用於查找不到local變量,將會繼續向上查找,查找上級最近的做用域,若是找到變量local,就會中止查找。若是找不到會一直查找到全局做用域,若是該變量在全部做用域都不存在,將會拋出未定義錯誤。

變量的主動釋放

var a = { sex : 10 };
b = 200;
window.c = 300; 
複製代碼

若是變量是全局變量,須要注意的是全局做用域中的變量不會執行垃圾回收過程,此類對象將會常駐內存(在老生代空間)。若是須要釋放該類對象空間,只能經過delete或從新賦值變量爲undefind或者null來釋放對象的引用。

閉包

什麼叫閉包,歷史爭議問題啊。咱們暫能夠將能使外部做用域訪問內部做用於中的變量的方法叫作閉包。

function foo(){
    var bar = function(){
        var local = "局部變量";
        return function(){
            return local;
        }
    }
    var baz = bar();
    console.log(baz())
}
複製代碼

閉包對垃圾回收帶來的影響也隨之出現,一旦有變量引用中間函數,這個中間函數將沒法被釋放,同時也會是該做用域沒法釋放,天然做用域中的變量也不會被釋放並回收,除非不在被引用,該函數纔會被逐漸釋放。

不得不說全局變量和閉包是項目中不可缺乏的角色,可是需對該類變量謹慎使用,防止在你的項目中這種沒法輕易被釋放的變量所佔內存愈來愈多,結果就是你不想看到的內存泄露。

結束了

這篇文章是從新回顧了一遍《深刻淺出Node.js》後,結合第5章的內存控制裏的內容和概念分享給你們的一篇基礎小文。這是一本13年的書,只能說書不怕老,內容清晰,頗有助於你們對一下經常使用的概念進行很細緻的瞭解。

有補充的評論加個知識點,沒補充捧個贊也好。

謝謝。

相關文章
相關標籤/搜索