一般狀況下,垃圾數據回收分爲手動回收和自動回收兩種策略。算法
咱們知道JavaScript中原始數據類型是存儲在棧空間中的,引用類型的數據是存儲在堆空間中的。經過這種分配方式,咱們解決了數據的內存分配的問題。
不過有些數據被使用以後,可能就再也不須要了,咱們把這種數據稱爲垃圾數據。若是這些垃圾數據一直保存在內存中,那麼內存會越用越多,因此咱們須要對這些垃圾數據進行回收,以釋放有限的內存空間。接下來咱們就分別說說棧數據和堆數據的回收函數
咱們經過如下代碼舉例來講性能
function outterFn(){ var a = 'out' var b = {value:"out"} function innerFn(){ var c = "in" var d = {value:"in"} } innerFn() } foo()
當程序運行到innerFn(),這時候調用棧和堆空間的快照是下圖這樣。
從圖中能夠看出,原始類型的數據被分配到棧中,引用類型的數據會被分配到堆中。當 outterFn 函數執行結束以後,outterFn 函數的執行上下文會從堆中被銷燬掉,那麼它是怎麼被銷燬的呢?下面咱們就來分析一下。學習
在以前的文章中,咱們介紹過,若是執行到函數時,那麼 JavaScript 引擎會建立函數的執行上下文,並將函數的執行上下文壓入到調用棧中,其調用棧就如上圖所示。與此同時,還有一個記錄當前執行狀態的指針(稱爲 ESP),指向調用棧中當前函數innerFn。當innerFn函數執行完成以後,函數執行流程就進入了下一個函數outterFn,那這時就須要銷燬innerFn函數的執行上下文了。ESP 這時候就幫上忙了,JavaScript 會將 ESP 下移到outterFn 函數的執行上下文,這個下移操做就是銷燬 innerFn 函數執行上下文的過程。動畫
經過上面的分析咱們知道,當上面那段代碼的 outterFn 函數執行結束以後,ESP 應該是指向全局執行上下文的,那這樣的話,outterFn 函數和 innerFn 函數的執行上下文就處於無效狀態了,不過保存在堆中的兩個對象依然佔用着空間。spa
要回收堆中的垃圾數據,就須要用到 JavaScript 中的垃圾回收器了。因此,接下來咱們就來分析下堆中的垃圾數據是如何回收的。線程
不過在正式介紹回收以前,咱們須要學習下代際假說(The Generational Hypothesis)的內容,這是垃圾回收領域中一個重要的術語,後續垃圾回收的策略都是創建在該假說的基礎之上的,因此非常重要。
代際假說有如下兩個特色:3d
有了代際假說的基礎,咱們就能夠來探討 V8 是如何實現垃圾回收的了。在 V8 中會把堆分爲新生代和老生代兩個區域指針
其實不論什麼類型的垃圾回收器,它們都有一套共同的執行流程。code
那麼接下來,咱們就按照這個流程來分析新生代垃圾回收器(副垃圾回收器)和老生代垃圾回收器(主垃圾回收器)是如何處理垃圾回收的。
一般狀況下,大多數小的對象都會被分配到新生區,因此說這個區域雖然不大,可是垃圾回收仍是比較頻繁的。新生代中用 Scavenge 算法來處理。所謂 Scavenge 算法,是把新生代空間對半劃分爲兩個區域,一半是對象區域,一半是空閒區域。
新加入的對象都會存放到對象區域,當對象區域快被寫滿時,就須要執行一次垃圾清理操做。在垃圾回收過程當中,首先要對對象區域中的垃圾作標記;標記完成以後,就進入垃圾清理階段,副垃圾回收器會把這些存活的對象複製到空閒區域中,同時它還會把這些對象有序地排列起來,因此這個複製過程,也就至關於完成了內存整理操做,複製後空閒區域就沒有內存碎片了。完成複製後,對象區域與空閒區域進行角色翻轉,也就是原來的對象區域變成空閒區域,原來的空閒區域變成了對象區域。這樣就完成了垃圾對象的回收操做,同時這種角色翻轉的操做還能讓新生代中的這兩塊區域無限重複使用下去。因爲新生代中採用的 Scavenge 算法,因此每次執行清理操做時,都須要將存活的對象從對象區域複製到空閒區域。但複製操做須要時間成本,若是新生區空間設置得太大了,那麼每次清理的時間就會太久,因此爲了執行效率,通常新生區的空間會被設置得比較小。也正是由於新生區的空間不大,因此很容易被存活的對象裝滿整個區域。爲了解決這個問題,JavaScript 引擎採用了對象晉升策略,也就是通過兩次垃圾回收依然還存活的對象,會被移動到老生區中。
除了新生區中晉升的對象,一些大的對象會直接被分配到老生區。所以老生區中的對象有兩個特色,一個是對象佔用空間大,另外一個是對象存活時間長。因爲老生區的對象比較大,若要在老生區中使用 Scavenge 算法進行垃圾回收,複製這些大的對象將會花費比較多的時間,從而致使回收執行效率不高,同時還會浪費一半的空間。於是,主垃圾回收器是採用標記 - 清除(Mark-Sweep)的算法進行垃圾回收的。
首先是標記過程階段。標記階段就是從一組根元素開始,遞歸遍歷這組根元素,在這個遍歷過程當中,能到達的元素稱爲活動對象,沒有到達的元素就能夠判斷爲垃圾數據。
從以前那副圖中咱們能夠大體看到垃圾數據的標記過程,當 innerFn函數執行結束以後,ESP 向下移動,指向了outterFn函數的執行上下文,這時候若是遍歷調用棧,是不會找到引用 1003 地址的變量,也就意味着 1003 這塊數據爲垃圾數據。因爲 1005 這塊數據被變量 b 引用了,因此這塊數據會被標記爲活動對象。這就是大體的標記過程。
接下來就是垃圾的清除過程。它和副垃圾回收器的垃圾清除過程徹底不一樣,可參考下圖大體理解下其清除過程:標記過程和清除過程就是標記 - 清除算法,不過對一塊內存屢次執行標記 - 清除算法後,會產生大量不連續的內存碎片。而碎片過多會致使大對象沒法分配到足夠的連續內存,因而又產生了另一種算法——標記 - 整理(Mark-Compact),這個標記過程仍然與標記 - 清除算法裏的是同樣的,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。
如今咱們知道了 V8 是使用副垃圾回收器和主垃圾回收器處理垃圾回收的,不過因爲 JavaScript 是運行在主線程之上的,一旦執行垃圾回收算法,都須要將正在執行的 JavaScript 腳本暫停下來,待垃圾回收完畢後再恢復腳本執行。咱們把這種行爲叫作全停頓(Stop-The-World)。好比堆中的數據有 1.5GB,V8 實現一次完整的垃圾回收須要 1 秒以上的時間,這也是因爲垃圾回收而引發 JavaScript 線程暫停執行的時間,如果這樣的時間花銷,那麼應用的性能和響應能力都會直線降低。
在 V8 新生代的垃圾回收中,因其空間較小,且存活對象較少,因此全停頓的影響不大,但老生代就不同了。若是在執行垃圾回收的過程當中,佔用主線程時間太久,主線程是不能作其餘事情的。好比頁面正在執行一個 JavaScript 動畫,由於垃圾回收器在工做,就會致使這個動畫在這期間沒法執行,這將會形成頁面的卡頓現象。爲了下降老生代的垃圾回收而形成的卡頓,V8 將標記過程分爲一個個的子標記過程,同時讓垃圾回收標記和 JavaScript 應用邏輯交替進行,直到標記階段完成,咱們把這個算法稱爲增量標記(Incremental Marking)算法。以下圖所示:
使用增量標記算法,能夠把一個完整的垃圾回收任務拆分爲不少小的任務,這些小的任務執行時間比較短,能夠穿插在其餘的 JavaScript 任務中間執行,這樣當執行上述動畫效果時,就不會讓用戶由於垃圾回收任務而感覺到頁面的卡頓了。