V8 的垃圾回收策略主要基於分代式垃圾回收機制。所謂分代式,就是將內存空間分爲新生代和老生代兩種,而後採用不一樣的回收算法進行回收。html
新生代空間中的對象爲存活時間較短的對象,大多數的對象被分配在這裏,這個區域很小可是垃圾回特別頻繁 。算法
它將堆內存一分爲二,每一部分空間稱爲 semispace,其中一個處於使用狀態(from 空間),另外一個處於閒置狀態(to 空間)閉包
對於新產生的對象,將從 from 空間中分配內存 。函數
新生代分配內存很是容易,咱們只須要保存一個指向內存區的指針,不斷根據新對象的大小進行遞增便可。當該指針到達了新生代內存區的末尾,就會觸發一次垃圾回收。性能
新生代的垃圾回收採用 Scavenge 算法 ,其工做原理以下:spa
首先檢查 from 空間,將存活對象複製到 to 空間,非存活對象將會被釋放。完成複製後,from 空間和 to 空間角色發生轉換。新產生的對象始終從 from 空間中分配內存,to 空間則處於閒置狀態。當再次進行垃圾回收時,也會執行和第一次一樣的操做,若是存在如下兩種狀況,存活對象就會被複制到老生代空間中,這個過程稱爲對象晉升。線程
老生代空間中的對象爲存活時間長或常駐內存對象,大多數重新生代晉升的對象會被移動到這裏。指針
老生代佔用內存較多,若是使用 Scavenge算法,不只會浪費一半空間,複製如此大塊的內存消耗時間將會很長,因此 Scavenge 算法顯然不適合。htm
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】限制堆內存大小
【2】增量式垃圾回收
V8 先從標記階段入手,將原來一口氣停頓完成的動做改成 增量標記(Incremental Marking),也就是拆分爲許多小步進,每作完一步進,就讓 JavaScript 應用邏輯執行一小會兒,垃圾回收與應用邏輯交替執行,直到標記階段完成。V8 後續還引入 Lazy Sweep(延遲清除)、Incremental Compaction (增量式整理),讓清理與整理動做也變成增量式的。同時還計劃引入並行標記與並行整理,進一步利用多核性能來下降每次停頓的時間。
做用域: 能造成做用域的函數調用、with 語句 以及 全局做用域。
閉包: V8 沒法主動回收內存中的閉包引用和全局變量引用。
一般,形成內存泄漏的緣由有以下幾個:
原創發佈 @一像素 2017.08
參考文獻:
[1] 樸靈,深刻淺出Node.js,人民郵電出版社,2013