快速學習nodejs系列:十一、nodejs垃圾回收

陳恆堅發表於2019-02-16

nodejs的垃圾回收機制是由v8引擎自動管理的。

nodejs的記憶體限制
在一般的後端語言(php)來說,記憶體的使用上是沒有限制的,但對於nodejs來說只能使用系統的部分—-64位系統為1.4G,32位系統位0.7G。這時如果你要處理個3G檔案進行資料分析,即使系統的記憶體為8G,在該nodejs程式記憶體還是會溢位。

造成上面這個問題主要是因為nodejs是基於v8的,nodejs是通過v8自己的方式來管理記憶體的。那v8為什麼要限制堆記憶體的大小呢?原因有2:
1.表層原因:v8是為瀏覽器設計的,不大可能遇到大記憶體的情景。
2.深層原因:v8垃圾回收機制的限制。以1.5G的堆記憶體為例,v8做一次小的垃圾回收需要50ms,做一次非增量的垃圾回收要1s。垃圾回收時會引起js的執行緒的暫停,在這樣的時間花銷下,應用的效能、響應能力會直線下降。
記憶體限制是可以開啟的:
–max-old-space-size(老生代)
–max-new-space-size(新生代)
v8堆記憶體大小 = 老生代 + 新生代

v8垃圾回收機制
v8垃圾回收主要是基於分代式垃圾回收機制。按物件的存活時間將記憶體的垃圾回收進行不同的分代,分別對不同的分代記憶體進行不同的演算法。

新生代—>存活時間較短的物件
老生代—>存活時間較長或常駐記憶體的物件
上面也說過,nodejs堆記憶體的大小是新生代記憶體空間加上老生代記憶體空間。

新生代演算法
新生代主要是通過scavenge演算法進行垃圾回收。

這是一種採用複製的方式來實現垃圾回收,將堆記憶體一分為二,每個空間稱為semispace。在這2個semispace空間中,只有一個處於使用中(稱為from空間),另一個處於空閒中(稱為to空間)。開始分配時首先從from空間開始,當開始垃圾回收時,也是從from空間開始檢查存活物件,把存活物件複製到to空間,而非存活物件佔用的空間就會被釋放。完成複製後,from空間和to空間角色對調。
從上面的過程可以知道,scavenge的缺點就是隻使用了一半的堆記憶體,犧牲空間獲取時間。

老生代通過mark-sweep、mark-comopact演算法。

mark-sweep標記清除,分為標記、清除2個階段。mark-sweep先在標記階段遍歷堆記憶體中的所有物件,並標記存活的物件;在清除階段把沒有被標記的物件清除。可以看出,scavenge只複製存活的物件,mark-sweep只清理死亡的物件。因為在新生代中存活的物件佔用小部分,而在老生代中死亡物件佔用小部分,這是這2中演算法高效的原因。

Mark-compact標記整理,mark-sweep中會出現一個問題,在回收後,對記憶體會出現不連續的狀態(記憶體碎片)。記憶體碎片會對後續的記憶體分配造成影響,因為會有這樣一種情況:需要分配個大記憶體,而所有記憶體碎片都無法完成分配,這會提前觸發垃圾回收,而這個回收時不必要的。Mark-compact是在Mark-sweep基礎上演變而來的,它主要區別在於:物件被標記後,在整理的過程中會將存活的物件都往一端移動,移動完成後直接清除。

小結:在正常的使用過程中,v8的記憶體限制還是夠用的,但nodejs的垃圾回收、單執行緒還是會影響效能。想要高效能,需要讓垃圾回收儘量小。在實際的開發中要老生代物件的使用,如實現web服務的會話(session),一般會通過記憶體來儲存(陣列),在訪問量大的情況下會導致老生代物件劇增,有可能造成溢位。如果要處理大記憶體的資料,比如讀取3G的檔案,我們會通過可讀流的pipe()方法,這樣就不會受到v8記憶體的限制影響,提高了nodejs程式的健壯性。

相關文章