Node-2.垃圾回收機制

NodeJS的垃圾回收機制

對於每一門編程語言來說,如何進行垃圾回收都是很重要的。

JavaScript和Java一樣,由垃圾回收機制來進行自動內存管理,不需要像C/C++程序員那樣需要在編寫代碼過程中關注內存的分配和釋放問題。

對於瀏覽器開發時很少遇到因爲垃圾回收對應用程序造成性能影響,當在服務器端,內存管理的好壞、垃圾回收狀況是否優良,對於性能敏感的服務器都會構成影響。

Node是基於V8構建,所以在Node中使用JavaScript對象基本都是通過V8自己的方式來進行分配和管理。在瀏覽器端是綽綽有餘,但是在Node作爲服務器時卻限制了大內存的使用,從而影響了性能。

V8提供了調整內存限制的方法可以在Node無法分配足夠內存給JavaScript時放寬V8默認的內存限制,從而避免執行過程中稍微多用一些內存就導致崩潰。

V8的垃圾回收機制

V8的垃圾回收策略主要基於分代式垃圾回收機制

在V8中,主要將內存分爲新生代老生代兩代。
新生代中的對象爲存活時間較短的對象
老生代中的對象爲存活時間較長或常駐內存的對象

V8堆的整體大小就是新生代所用內存空間加上老生代的內存空間。新生代和老生代內存空間的最大值限制可以在啓動時就指定。
V8內存無法根據使用情況自動擴充,當內存分配超過極限值,會引起進程出錯。

V8默認設置下,64位系統老生代爲1400MB,32位系統下爲700MB;
新生代內存最大值在64位系統和32位系統上分別爲32MB和16MB。
默認V8堆內存的最大值在64位系統上爲1464 MB,也就是約1.4GB;32位系統上則爲732MB,也就是約0.7GB。

V8的堆內存示意圖

新生代 —— 使用Scavenge算法進行垃圾回收

在分代的基礎上,新生代中的對象主要通過Scavenge算法進行垃圾回收。

Scavenge算法

Scavenge具體實現主要才用了Cheney算法
Cheney算法是一種採用複製的方式實現的垃圾回收算法。將堆內存一分爲二,每一部分空間稱爲semispace。在這兩個semispace空間中,處於使用狀態的semispace空間稱爲From空間,處於閒置狀態的空間稱爲To空間

當分配對象時,先是在From空間中進行分配.當開始進行垃圾回收時,會檢查From空間中的存活對象,這些存活對象將被複制到To空間中,而非存活對象佔用的空間將會被釋放。完成複製後,From空間和To空間的角色發生對換。

簡而言之,在新生代垃圾回收的過程中,就是通過將存活對象在兩個semispace空間之間進行復制

Scavenge是典型的犧牲空間換取時間的算法,所以無法大規模應用到所有垃圾回收中,但是非常適合應用在新生代中,因爲只複製存活對象,而新生代中對象的生命週期比較短(存活下來的對象少),在時間上有優異的表現。

實際使用的堆內存是新生代中的兩個semispace空間大小和老生代所用內存大小之和。

當一個對象經過多次複製依然存活時(From空間中的存活對象在複製到To空間之前需要進行檢查),它將會被認爲是生命週期較長的對象。這種較長生命週期的對象隨後會被移動到老生代中,採用新的算法進行管理。對象從新生代中移動到老生代中的過程稱爲晉升

晉升到老生代的條件爲:

  • 是否經歷過Scavenge回收
  • 從From空間複製對象到To空間時,如果To空間已經使用超過25%
    設置25%的限制值是爲了當前完成回收後,當前空間變成From空間,如果佔比過高,會影響後續內存分配。

老生代 —— Mark-Sweep 和 Mark-Compact

Mark-Sweep

Mark-Sweep是標記清除的意思,它分爲標記清除兩個階段。

Mark-Sweep在標記階段遍歷堆中的所有對象,並標記活着的對象,在隨後的清除階段中,只清除沒有被標記的對象

Mark-Sweep最大的問題是在進行一次標記清除回收後,內存空間會出現不連續的狀態。這種內存碎片會對後續的內存分配造成問題。

Mark-Compact

Mark-Compact標記整理的意思,和Mark-Sweep的差別在於對象在標記爲死亡後,在整理的過程中,將活着的對象往一端移動,移動完成後,直接清理掉邊界外的內存,解決Mark-Sweep的內存碎片問題。

由於Mark-Compact需要移動對象,所以它的執行速度不可能很快,所以在取捨上,V8主要使用Mark-Sweep,在空間不足以對從新生代中晉升過來的對象進行分配時才使用Mark-Compact。

3種垃圾回收算法的簡單對比:

回收算法 Mark-Sweep(標記清除算法) Mark-Compact(標記整理算法) Scavenge
速度 中等 最慢 最快
空間開銷 少(有碎片) 少(無碎片) 雙倍空間(無碎片)
是否移動對象

Incremental Marking

應用背景:
爲了避免JavaScrip應用邏輯和垃圾回收器同時執行導致的混亂,垃圾回收執行時需要將應用邏輯暫停下來,等執行完垃圾回收後再恢復執行應用邏輯,這種行爲被稱爲 “全停頓”(stop-the-world)

在V8的分代式垃圾回收中,一次小垃圾回收只收集新生代,由於新生代默認配置得較小,且其中存活對象通常較少,所以即便它是全停頓的影響也不大。但V8的老生代通常配置得較大,且存活對象較多,全堆垃圾回收(full 垃圾回收)的標記、清理、整理等動作造成的停頓就會比較可怕。

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

V8在經過增量標記的改進後,垃圾回收的最大停頓時間可以減少到原本的1/6左右。

V8後續還引入了延遲清理(lazy sweeping)增量式整理(incremental compaction),讓清理與整理動作也變成增量式的。同時還計劃引入並行標記與並行清理,進一步利用多核性能降低每次停頓的時間。

垃圾回收是影響性能的因素之一,想要高性能的執行效率,需要注意讓垃圾回收儘量少地進行,尤其是全堆垃圾回收。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章