V8引擎的垃圾回收策略


V8 的垃圾回收策略主要基於分代式垃圾回收機制。所謂分代式,就是將內存空間分為新生代和老生代兩種,然后采用不同的回收算法進行回收。

新生代空間

新生代空間中的對象為存活時間較短的對象,大多數的對象被分配在這里,這個區域很小但是垃圾回特別頻繁 。

它將堆內存一分為二,每一部分空間稱為 semispace,其中一個處於使用狀態(from 空間),另一個處於閑置狀態(to 空間)

對於新產生的對象,將從 from 空間中分配內存 。

新生代分配內存非常容易,我們只需要保存一個指向內存區的指針,不斷根據新對象的大小進行遞增即可。當該指針到達了新生代內存區的末尾,就會觸發一次垃圾回收。

新生代的垃圾回收采用 Scavenge 算法 ,其工作原理如下:

首先檢查 from 空間,將存活對象復制到 to 空間,非存活對象將會被釋放。完成復制后,from 空間和 to 空間角色發生轉換。新產生的對象始終從 from 空間中分配內存,to 空間則處於閑置狀態。當再次進行垃圾回收時,也會執行和第一次同樣的操作,如果存在以下兩種情況,存活對象就會被復制到老生代空間中,這個過程稱為對象晉升

  • 存活對象已經經歷過一次 Scavenge 回收 。
  • to 空間內存占用比例超過 25% (保證下次新對象有足夠的空間可分配)

老生代空間

老生代空間中的對象為存活時間長或常駐內存對象,大多數從新生代晉升的對象會被移動到這里。

老生代占用內存較多,如果使用 Scavenge算法,不僅會浪費一半空間,復制如此大塊的內存消耗時間將會很長,所以 Scavenge 算法顯然不適合。

V8 對於老生代中的垃圾回收,采用 Mark-Sweep (標記清除) 和 Mark-Compact(標記整理) 相結合 。

【1】Mark-Sweep

Mark-Sweep 分為 標記 和 清除 兩個階段 。

在標記階段需要遍歷堆中的所有對象,並標記那些活着的對象,然后進入清除階段。在清除階段,只清除沒有被標記的對象。由於標記清除只清除死亡對象,而死亡對象在老生代中占用的比例很小,所以效率較高。

標記清除存在的問題是,進行一次標記清除后,內存空間往往是不連續的,會出現很多的內存碎片。如果后續需要分配一個需要內存空間較多的對象時,如果所有的內存碎片都不夠用,將會使得V8無法完成這次分配,提前觸發垃圾回收。

【2】Mark-Compact

標記整理正是為了解決標記清除所帶來的內存碎片的問題。標記整理在標記清除的基礎進行修改,將其的清除階段變為緊縮極端。在整理的過程中,將活着的對象向內存區的一段移動,移動完成后直接清理掉邊界外的內存。緊縮過程涉及對象的移動,所以效率並不是太好,但是能保證不會生成內存碎片。  

三種回收策略比較

從圖中可以看出,在 Mark-Sweep 和 Mark-Compact 之間,由於 Mark-Compact 需要移動對象,所以它的執行速度最慢。

所以在取舍上,V8 主要使用 Mark-Sweep,在空間不足以對新生代中晉升過來的對象進行分配時才使用 Mark-Compact 。

垃圾回收引起的性能問題

為了避免出現 JavaScript 應用邏輯 與 垃圾回收操作 產生不一致的沖突,垃圾回收的三種基本算法都需要將應用邏輯暫停下來,待垃圾回收完成后,再恢復執行應用邏輯,這種行為被稱為全停頓 。

按官方說法,以 1.5G 的垃圾回收堆內存為例,V8 做一次小的垃圾回收需要 50ms 以上,做一次非增量式垃圾回收甚至需要 1s 以上。這是垃圾回收中引起的 JavaScript 線程暫停執行時間,在這樣的時間花銷下,應用性能和響應能力都會直線下降。

在 V8 的分代式垃圾回收中,一次小垃圾回收只收集新生代,由於新生代默認配置的較小,且其中活動對象通常較少,所以即便它是全停頓,影響也不大。

但 V8 的老生代通常配置較大,且存活對象較多,全堆垃圾回收的標記、清理、整理等動作造成的停頓就會比較嚴重。

為降低全堆垃圾回收而導致的停頓時間,V8 做了以下改善措施:

【1】限制堆內存大小

  •  新生代:64 位系統 和 32 位系統分別為 32M 和 16 M (from 和 to 空間各占一半)
  •  老生代:64 位系統 和 32 位系統分別為 1400M 和 700 M

【2】增量式垃圾回收

V8 先從標記階段入手,將原來一口氣停頓完成的動作改為 增量標記(Incremental Marking),也就是拆分為許多小步進,每做完一步進,就讓 JavaScript 應用邏輯執行一小會兒,垃圾回收與應用邏輯交替執行,直到標記階段完成。V8 后續還引入 Lazy Sweep(延遲清除)、Incremental Compaction (增量式整理),讓清理與整理動作也變成增量式的。同時還計划引入並行標記與並行整理,進一步利用多核性能來降低每次停頓的時間。

垃圾回收的觸發條件

 作用域: 能形成作用域的函數調用、with 語句 以及 全局作用域。

 閉包:  V8 無法主動回收內存中的閉包引用和全局變量引用。

內存泄漏

通常,造成內存泄漏的原因有如下幾個:

  • 隊列消費不及時
  • 作用域未釋放

 

原創發布 @一像素 2017.08

 

參考文獻:

[1]  朴靈,深入淺出Node.js,人民郵電出版社,2013


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM