V8記憶體管理及垃圾回收機制

木子星兮發表於2020-07-10

JavaScript引擎的記憶體空間主要分為棧和堆。

棧是臨時儲存空間,主要儲存區域性變數和函式呼叫。

基本型別資料(Number, Boolean, String, Null, Undefined, Symbol, BigInt)儲存在在棧記憶體中。
引用型別資料儲存在堆記憶體中,引用資料型別的變數是一個指向堆記憶體中實際物件的引用,存在棧中。

基本型別賦值,系統會為新的變數在棧記憶體中分配一個新值,這個很好理解。引用型別賦值,系統會為新的變數在棧記憶體中分配一個值,這個值僅僅是指向同一個物件的引用,和原物件指向的都是堆記憶體中的同一個物件。

對於函式,直譯器建立了”呼叫棧“來記錄函式的呼叫過程。每呼叫一個函式,直譯器就可以把該函式新增進呼叫棧,直譯器會為被新增進來的函式建立一個棧幀(用來儲存函式的區域性變數以及執行語句)並立即執行。如果正在執行的函式還呼叫了其他函式,新函式會繼續被新增進入呼叫棧。函式執行完成,對應的棧幀立即被銷燬。

兩種檢視呼叫棧的方法

  1. 使用 console.trace()") 向Web控制檯輸出一個堆疊跟蹤.
  2. 瀏覽器開發者工具進行斷點除錯

棧雖然很輕量,在使用時建立,使用結束後銷燬,但是不是可以無限增長的,被分配的呼叫棧空間被佔滿時,就會引起”棧溢位“的錯誤。

(function foo() {
    foo()
})()

Maximum call stack size exceeded

為什麼基本資料型別儲存在棧中,引用資料型別儲存在堆中?

JavaScript引擎需要用棧來維護程式執行期間的上下文的狀態,如果棧空間大了的話,所有資料都存放在棧空間裡面,會影響到上下文切換的效率,進而影響整個程式的執行效率。

堆空間儲存的資料比較複雜,大致可以劃分為下面 5 個區域:程式碼區(Code Space)、Map 區(Map Space)、大物件區(Large Object Space)、新生代(New Space)、老生代(Old Space)。本篇文章主要討論新生代和老生代的記憶體回收演算法。

新生代記憶體是臨時分配的記憶體,存活時間段,老生代記憶體是常駐記憶體,存活時間長。

新生代記憶體和老生代記憶體

新生代記憶體回收

新生代中用 Scavenge 演算法來處理。所謂 Scavenge 演算法,是把新生代空間對半劃分為兩個區域,一半是物件區域(from),一半是空閒區域 (to)。

新的物件會首先被分配到 from 空間,當進行垃圾回收的時候,會先將 from 空間中的 存活的物件複製到 to 空間進行儲存,對未存活的物件的空間進行回收。
複製完成後, from 空間和 to 空間進行調換,to 空間會變成新的 from 空間,原來的 from 空間則變成 to 空間。這種演算法稱之為 ”Scavenge“。

Scavenge 演算法

新生代記憶體回收頻率很高,速度也很快,但是空間利用率很低,因為有一半的記憶體空間處於"閒置"狀態。

老生代記憶體回收

新生代中多次進行回收仍然存活的物件會被轉移到空間較大的老生代記憶體中,這種現象稱為晉升。以下兩種情況

  1. 在垃圾回收過程中,發現某個物件之前被清理過,那麼將會晉升到老生代的記憶體空間中
  2. 在 from 空間和 to 空間進行反轉的過程中,如果 to 空間中的使用量已經超過了 25% ,那麼就講 from 中的物件直接晉升到老生代記憶體空間中。

因為老生代空間較大,如果仍然用 Scavenge 演算法來頻繁複制物件,那麼效能開銷就太大了。

標記-清除(Mark-Sweep)

老生代採用的是”標記清除“來回收未存活的物件。

分為標記和清除兩個階段。標記階段會遍歷堆中所有的物件,並對存活的物件進行標記,清除階段則是對未標記的物件進行清除。
標記清除過程

標記-整理(Mark-Compact)

標記清除不會對記憶體一分為二,所以不會浪費空間。但是經過標記清除之後的記憶體空間會生產很多不連續的碎片空間,這種不連續的碎片空間中,在遇到較大的物件時可能會由於空間不足而導致無法儲存。
為了解決記憶體碎片的問題,需要使用另外一種演算法 - 標記-整理(Mark-Compact)。標記整理對待未存活物件不是立即回收,而是將存活物件移動到一邊,然後直接清掉端邊界以外的記憶體。

標記整理過程

增量標記

為了避免出現JavaScript應用程式與垃圾回收器看到的不一致的情況,進行垃圾回收的時候,都需要將正在執行的程式停下來,等待垃圾回收執行完成之後再回復程式的執行,這種現象稱為“全停頓”。如果需要回收的資料過多,那麼全停頓的時候就會比較長,會影響其他程式的正常執行。
垃圾回收過程

為了避免垃圾回收時間過長影響其他程式的執行,V8將標記過程分成一個個小的子標記過程,同時讓垃圾回收和JavaScript應用邏輯程式碼交替執行,直到標記階段完成。我們稱這個過程為增量標記演算法。
增量標記
通俗理解,就是把垃圾回收這個大的任務分成一個個小任務,穿插在 JavaScript任務中間執行,這個過程其實跟 React Fiber 的設計思路類似。

參考

相關文章