概念
原始資料類型是存儲在棧空間中的,引用類型的資料是存儲在堆空間中的。
不過有些資料被使用之後,可能就不再需要了,我們把這種資料稱為垃圾資料。
如果這些垃圾資料一直儲存在記憶體中,那麼記憶體會越用越多。
是以我們需要對這些垃圾資料進行回收,以釋放有限的記憶體空間。
這時候就引入了垃圾回收機制。
棧垃圾回收
當函數執行結束,JavaScript 引擎通過向下移動
ESP
指針(記錄調用棧目前執行狀态的指針),來銷毀該函數儲存在棧中的執行上下文(變量環境、詞法環境、
this
、
outer
)。
function fn(){
var a = 1;
var b = { age: '2' };
function fn2(){
var c = 3;
var d = { name: 'xiaoming' };
}
fn2();
}
fn();
在上面代碼中,根據浏覽器政策:
- 先到全局執行上下文
- 再到 foo 函數執行上下文
- 最後到 showName 函數執行上下文
根據棧的特性,此時 JavaScript 情況應為:
調用棧 |
函數執行上下文 |
fn函數執行上下文 |
全局執行上下文 |
釋放過程:
- 執行到 fn2 函數的時候,建立 fn2 函數的執行上下文,并将 fn2 函數的執行上下文壓入棧中。
- 以此同時,我們會有一個記錄目前執行狀态的指針(稱為 ESP),指向調用棧中 fn2 函數的執行上下文,表示目前正在執行 fn2 函數。
- 當 fn2 函數執行完畢,函數執行流程進入 fn 函數,這時候 ESP 就下移到 fn 函數的執行上下文。這個下移操作就是銷毀 fn2 函數執行上下文的過程。
如果 fn2 函數下面還調用了另一個函數,那麼它會覆寫 fn2 函數的内容,用來存放另一個函數的執行上下文。
就是說:當一個函數執行結束之後,JavaScript 引擎會通過向下移動
ESP
來銷毀儲存棧中的執行上下文。
堆垃圾回收
上述代碼中的對象依舊存在堆中要回收對象的垃圾資料,就需要 JavaScript 中的垃圾回收器。
V8 中會把堆分為 新生代 和 老生代 兩個區域,新生代中存放的是生存時間短的對象,老生代中存放的生存時間久的對象。
新生區的容量沒有老生區那麼大,是以 V8 分别使用 2 個不同的垃圾回收器,來高效實施垃圾回收:
- 副垃圾回收器:主要負責新生代的垃圾回收。
- 主垃圾回收器:主要負責老生代的垃圾回收。
新生代 - 副垃圾回收器
算法:Scavenge 算法。
原理:
- 把新生代空間對半劃分為兩個區域,一半是對象區域,一半是空閑區域。
- 新加入的對象都會存放到對象區域,當對象區域快被寫滿時,就需要執行一次垃圾清理操作。
- 先對對象區域中的垃圾做标記,标記完成之後,把這些存活的對象複制到空閑區域中
- 完成複制後,對象區域與空閑區域進行角色翻轉,也就是原來的對象區域變成空閑區域,原來的空閑區域變成了對象區域。
對象晉升政策:經過兩次垃圾回收依然還存活的對象,會被移動到老生區中。
老生代 - 主垃圾回收器
算法:标記 - 清除(Mark-Sweep)算法。
原理:
- 标記:标記階段就是從一組根元素開始,遞歸周遊這組根元素,在這個周遊過程中,能到達的元素稱為活動對象,沒有到達的元素就可以判斷為垃圾資料。
- 清除:将垃圾資料進行清除。
對一塊記憶體多次執行标記 - 清除算法後,會産生大量不連續的記憶體碎片。而碎片過多會導緻大對象無法配置設定到足夠的連續記憶體。
這時候就需要标記整理。
算法:标記 - 整理(Mark-Compact)算法
原理:
- 标記:和标記 - 清除的标記過程一樣,從一組根元素開始,遞歸周遊這組根元素,在這個周遊過程中,能到達的元素标記為活動對象。
- 整理:讓所有存活的對象都向記憶體的一端移動
- 清除:清理掉端邊界以外的記憶體
全停頓
由于 JavaScript 是單線程的,是以一旦執行垃圾回收算法,那正在執行的 JavaScript 腳本需要暫停下來,等垃圾回收完畢之後再恢複腳本執行。
這種行為叫 全停頓(Stop-The-World)。
如果堆中的資料較多,那麼回收需要時間,會造成頁面卡頓狀态,是以為了降低這個卡頓,V8 将标記過程分為一個一個子标記過程,同時垃圾回收标記和 JavaScript 應用邏輯交替進行。
優化算法:增量标記(Incremental Marking)算法
原理:
- 為了降低老生代的垃圾回收而造成的卡頓
- V8 把一個完整的垃圾回收任務拆分為很多小的任務
- 讓垃圾回收标記和 JavaScript 應用邏輯交替進行