V8垃圾回收機制

一、NodeJs執行環境

我們都知道在chrome瀏覽器內JavaScript是在v8上解釋執行的,NodeJs就是基於JavaScript環境和語言編寫的,它的執行環境也就離不開v8了。放張圖來說明一下瀏覽器和NodeJs執行環境的對比。

Chrome瀏覽器和Node的組件構成對比

上圖可用看到,在瀏覽器中因爲需要渲染UI界面(HTML,CSS),所以多出來了瀏覽器的WebKit內核以及底層也會調用到顯卡來顯示渲染界面。

而NodeJs的使用場景多是在服務器端,也就是說沒有UI界面需要渲染,所以也就不在需要WebKit內核,底層相應的也就不會去調用到顯卡驅動。NodeJs的中間層libuv是其支持跨平臺運行的關鍵所在。

上圖也充分說明了在Chrome瀏覽器和NodeJs中JavaScript的執行環境都是V8,今天我們要說的重點也就是V8虛擬機的垃圾回收機制。

二、V8的內存限制

在其它後端語言中,例如java在使用內存時就沒限制(可用充分使用物理內存)。但是在JavaScript中,內存的使用就受到了V8虛擬機的限制。因爲V8是爲Chrome瀏覽器研發產生的,在瀏覽器環境內並不會存在使用大量內存的情況,所以V8在內存使用上對不同平臺做了相對應的限制:32位操作系統約爲0.7GB,64位操作系統約爲1.4GB。這也就意味着當我們在操作大文件時(超過該內存限制的文件),無法將文件全部讀取到內存中來,相對應的計算機的內存資源也就無法得到充分的使用。

三、V8對象分配(堆內存)

瞭解JVM的人都知道,java中的對象分配是在堆內存中的。V8也不例外,當我們在代碼中聲明變量並賦值時,所使用對象的內存就分配在堆中。如果已申請的堆空閒內存不夠分配新對象時,將繼續申請堆內存,直到堆的大小超過V8的限制爲止。

查看堆內存使用情況:

$ node
> process.memoryUsage();
{ rss: 20774912,
  heapTotal: 7258112,
  heapUsed: 4225360,
  external: 9327 }

以上內存限制在Node中也是可用打破的,在啓動項目時指定堆內存大小,如下所示:

node --max-old-space-size=2048 index.js // 設置老齡代堆內存大小,單位MB
// 或者
node --max-new-space-size=1024 index.js // 設置新生代堆內存大小,單位MB

以上參數在V8初始化時生效,一旦生效就不能再動態修改。

四、V8的垃圾回收機制

V8的垃圾回收策略主要基於分代垃圾回收機制。在分代回收的基礎上,針對新生代和老齡代在其內部又做了相對應的回收算法:新生代內使用的是複製算法、老齡代內使用的是標記-清除算法以及標記-合併算法。

1、內存堆內存分配-分代算法

V8的堆內存分代算法

將整個堆內存分爲新生代和老齡代,新生代內保存的是最新創建且存活週期短的對象。老齡代內保存的是較爲活躍且存活週期長的對象。

2、複製算法(新生代內存)

該算法將內存一分爲二,每一部分空間稱爲semispace。在這兩個semispace空間中,只有一個處於使用中,另一個處於閒置狀態。正在使用的semispace空間我們稱之爲From空間,處於閒置的semispace空間我們稱之爲To空間。當我們分配對象時,先是在From空間進行分配。當新生代內存中開始垃圾回收時,會檢查From空間中的存活對象,這些存活對象將被複制到To空間中,而非存活對象佔用的空間將被釋放。完成複製後,From空間和To空間的角色發生互換,From空間變爲To空間,To空間變爲From空間。簡而言之,在新生代的垃圾回收過程中,就是通過將存活對象在兩個semispace空間之間進行復制。如下圖所示:

V8堆內存中新生代內存

複製算法的缺點是隻能使用新生代內存中的一半內存空間大小。但複製算法由於只複製存活的對象,並且對於生命週期短的場景存活對象只佔少部分,所以它在時間效率上有優異的表現。

由於複製算法是典型的犧牲空間換取時間的算法,所以無法大規模應用在所有的垃圾回收中。但可以發現,複製算法非常適合應用在新生代中,因爲新生代中對象的生命週期較短,恰恰適合這個算法。

由上圖也可以看出,實際在V8中處於使用的內存是新生代一半的內存加上老齡代內存,有一半的新生代內存處於閒置狀態。

我們瞭解了新生代中的內存空間使用,那什麼樣的對象會從新生代中晉升到老齡代內存中呢?當一個對象經過多次複製依然存活時,它將會被認爲是生命週期較長的對象。這種較長生命週期的對象隨後會被移動到老齡代內存中,採用老齡代中新的算法進行管理。

對象從新生代晉升到老齡代的條件主要有兩個:一個是對象是否經歷過複製算法回收,一個是To空間的內存佔用比超過一定的限制。

3、標記-清除算法(老齡代內存)

標記清除算法分爲兩個階段,一階段是標記,另一階段是清除。與複製算法相比,標記-清除算法並不會將內存空間劃分爲兩半,所以不存在浪費一半空間的行爲。與複製算法不同的是,標記-清除算法在標記階段遍歷堆中的所有對象,並標記活着的對象,在隨後的清除階段中,只清除沒有被標記的對象。可以看出複製算法只複製活着的對象,而標記-清除算法只清理死亡的對象。這是因爲活對象在新生代中佔較小部分,死對象在老齡代中佔較小部分,這也是兩種回收方式能高效工作的原因。標記-清除算法的工作圖解如下:

標記-清除算法工作時內存圖

標記-清除算法最大的問題是在進行一次標記清除回收後,內存空間會出現不連續的狀態。這些內存碎片會對後續的內存分配造成問題,因爲很可能出現需要分配一個大內存對象的情況,這時無法分配的情況下就會再次觸發垃圾回收,而這次的回收是不必要的,這時就該下面的回收算法登場了:標記-合併算法。

4、標記-合併算法(老齡代內存)

上面也說到了標記-清除算法存在的弊端,標記-合併算法就是爲了解決這些弊端而設計演變出來的。它們的差別在於對象在標記爲死亡後,在整理的過程中,將活着的對象往一端移動,在移動完成後,直接清理掉另一端死亡的對象。完成移動並清理完另一端死亡對象的內存後,老齡代內存空間就是連續的未使用和以使用了,這樣就可以進行大內存對象的分配了。

5、垃圾回收算法比較

回收算法複製算法標記-清除算法標記-合併算法
速度中等最慢最快
空間開銷少(有碎片)少(無碎片)雙倍空間(無碎片)
是否移動對象
發佈了89 篇原創文章 · 獲贊 29 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章