理解 Node.js 的 GC 機制

《深刻淺出Node.js》第五章《內存控制》閱讀筆記node

隨着 Node 的發展,JavaScript 的應用場景早已再也不侷限在瀏覽器中。本文不討論網頁應用、命令行工具等短期執行,且隻影響終端用戶的場景。因爲運行時間短,隨着進程的退出,內存會釋放,幾乎沒有內存管理的必要。但隨着 Node 在服務端的普遍應用,JavaScript 的內存管理須要引發咱們的重視。算法

V8 的內存限制

在通常的後端開發語言中,在基本的內存使用上沒有什麼限制,然而在 Node 中經過 JavaScript 使用內存時就會發現只能使用部份內存(64位系統下約爲1.4GB,32位系統下約爲0.7GB)。在這樣的限制下,將會致使 Node 沒法直接操做大內存對象。後端

形成這個問題的主要緣由在於 Node 的 JavaScript 執行引擎 V8。瀏覽器

在 V8 中,全部的 JavaScript 對象都是經過堆來進行分配的。Node 提供了 V8 中內存的使用量查看方法 process.memoryUsage()工具

  • heapTotal 已申請到的堆內存
  • heapUsed 當前使用的堆內存

爲何 V8 要限制堆的大小:spa

  1. V8 爲瀏覽器而設計,不太可能遇到用大量內存的場景
  2. V8 的垃圾回收機制的限制。(按官方的說法,以1.5GB的垃圾回收堆內存爲例,V8作一次小的垃圾回收須要50ms以上,作一次非增量式的垃圾回收須要1s以上)

V8提供了選項讓咱們能夠控制使用內存的大小命令行

  • node --max-old-space-size=1700 test.js 設置老生代內存空間最大值,單位爲MB
  • node --max-new-space-size=1024 test.js 設置新生代內存空間最大值,單位爲KB

比較遺憾的是,這兩個最大值須要在啓動時執行。這意味着 V8 使用的內存沒辦法根據使用的狀況自動擴充,當內存分配過程當中超過極限值時,就會引發進程出錯。設計

V8 的垃圾回收機制

V8 的垃圾回收策略主要基於分代式垃圾回收機制。在 V8 中,主要將內存分爲新生代和老生代兩代。新生代中的對象爲存活時間較短的對象,老生代中的對象爲存活時間較長或常駐內存的對象。3d

V8的分代示意圖

V8 堆的總體大小就是新生代的內存空間加上老生代的內存空間日誌

Scavenge 算法

在分代的基礎上,新生代中的對象主要經過 Scavenge 算法進行垃圾回收。在 Scavenge 的具體實現中,主要採用了 Cheney 算法。

Cheney 算法是一種採用複製的方式實現的垃圾回收算法。它將堆內存一分爲二,每一部分空間成爲 semispace。在這兩個 semispace 空間中,只有一個處於使用中,另外一個處於閒置中。處於使用中的 semispace 空間成爲 From 空間,處於閒置狀態的空間成爲 To 空間。當咱們分配對象時,先是在 From 空間中進行分配。當開始進行垃圾回收時,會檢查 From 空間中的存活對象,這些存活對象將被複制到 To 空間中,而非存活對象佔用的空間將被釋放。完成複製後, From 空間和 To 空間的角色發生對換。

Scavenge 的缺點是隻能使用堆內存的一半,但 Scavenge 因爲只複製存活的對象,而且對於生命週期短的場景存活對象只佔少部分,因此它在時間效率上表現優異。Scavenge 是典型的犧牲空間換取時間的算法,沒法大規模地應用到全部的垃圾回收中,但很是適合應用在新生代中。

V8中的堆內存示意圖

晉升

對象重新生代中移動到老生代中的過程稱爲晉升。

From 空間中的存活對象在複製到 To 空間以前須要進行檢查,在必定條件下,須要將存活週期長的對象移動到老生代中,也就是完成對象的晉升。

晉升條件主要有兩個:

  1. 對象是否經歷過一次 Scavenge 回收
  2. To 空間已經使用超過 25%

設置 25% 這個限制值得緣由是當此次 Scavenge 回收完成後,這個 To 空間將變成 From 空間,接下來的內存分配將在這個空間中進行,若是佔比太高,會影響後續的內存分配。

Mark-Sweep & Mark-Compact

V8 在老生代中主要採用了 Mark-Sweep 和 Mark-Compact 相結合的方式進行垃圾回收。

Mark-Sweep 是標記清楚的意思,它分爲兩個階段,標記和清除。Mark-Sweep 在標記階段遍歷堆中的全部對象,並標記活着的對象,在隨後的清除階段中,只清除未被標記的對象。

Mark-Sweep 最大的問題是在進行一次標記清除回收後,內存空間會出現不連續的狀態。這種內存碎片會對後續的內存分配形成問題,由於極可能出現須要分配一個大對象的狀況,這時全部的碎片空間都沒法完成這次分配,就會提早觸發垃圾回收,而此次回收是沒必要要的。

爲了解決 Mark-Sweep 的內存碎片問題,Mark-Compact 被提出來。Mark-Compact是標記整理的意思,是在 Mark-Sweep 的基礎上演進而來的。它們的差異在於對象在標記爲死亡後,在整理過程當中,將活着的對象往一端移動,移動完成後,直接清理掉邊界外的內存。

下表爲3種主要垃圾回收算法的簡單比較

從表中能夠看出,在 Mark-Sweep 和 Mark-Compact 之間,因爲 Mark-Compact 須要移動對象,因此它的執行速度不可能很快,因此在取捨上,V8 主要使用 Mark-Sweep,在空間不足以重新生代中晉升過來的對象進行分配時才使用 Mark-Compact 。

Incremental Marking

爲了不出現 JavaScript 應用邏輯與垃圾回收器看到的不一致的狀況,垃圾回收的3種算法都須要將應用邏輯暫停下來,這種行爲稱爲「全停頓」 (stop-the-world)。

因爲新生代配置的空間較小,存活對象較少,全停頓對新生代影響不大。但老生代一般配置的空間較大,且存活對象較多,全堆垃圾回收(full 垃圾回收)的標記、清除、整理等動做形成的停頓就會比較可怕。

爲了下降全堆垃圾回收帶來的停頓時間,V8 先從標記階段入手,將本來要一口氣停頓完成的動做改爲增量標記(Incremental Marking),也就是拆分爲許多小「步進」,每作完一「步進」就讓JavaScript應用邏輯執行一小會兒,垃圾回收和應用邏輯交替執行直到標記階段完成。

V8 在通過增量標記的改進後,垃圾回收的最大停頓時間能夠減小到本來的 1/6 左右。

查看GC日誌

查看垃圾回收日誌的方式主要是在啓動時添加 --trace_gc 參數。

小結

  1. Node 的 JavaScript 執行引擎爲 V8,內存使用和控制也受限於 V8。
  2. V8 把內存分爲新生代和老生代,分別存放存活時間較短和存活時間較長或常駐內存的對象。
  3. 在新生代中使用 Scavenge 算法進行垃圾回收,優勢是速度快無內存碎片,缺點是佔用雙倍內存空間。
  4. 在老生代中將 Mark-Sweep 和 Mark-Compact 兩種算法結合使用,主要使用 Mark-Sweep,優勢的是無需移動對象,缺點是產生內存碎片。Mark-Compact 是對 Mark-Sweep 的補充,在空間不足以對新晉升的對象進行分配時整理內存,清除內存碎片,因爲要移動對象,速度較慢。
  5. V8 使用 Incremental Marking 來減小全停頓帶來的影響。
相關文章
相關標籤/搜索