NodeJS中被忽略的內存

原文連接:BlueSun | NodeJS中被忽略的內存javascript

如樸靈說過,Node對內存泄露十分敏感,一旦線上應用有成千上萬的流量,那怕是一個字節的內存泄漏也會形成堆積,垃圾回收過程當中將會耗費更多時間進行對象掃描,應用響應緩慢,直到進程內存溢出,應用奔潰。html

雖然從好久之前就知道內存問題是不容忽視的,可是平常開發的時候並無碰到性能上的瓶頸,直到最近作了一個百萬PV級的營銷項目,因爲訪問量,併發量都達到了一個量級。一些細小的、平時沒注意到的問題被放大,這才映入眼簾,開始注意到了內存問題。卻不知Node對內存的泄露是如此的敏感。
爲此,趕忙去補習了一下V8中的內存處理機制。
那麼,V8中的內存機制是怎麼樣的?前端

V8的內存機制

內存的限制

Node中並不像其餘後端語言中,對內存的使用沒有多少限制。在Node中使用內存,只能使用到系統的一部份內存,64位系統下約爲1.4GB,32位系統下約爲0.7GB。這歸咎於Node使用了原本運行在瀏覽器的V8引擎。java

V8引擎的設計之初只是運行在瀏覽器中,而在瀏覽器的通常應用場景下使用起來綽綽有餘,足以勝任前端頁面中的全部需求。node

雖然服務端操做大內存也不是常見的需求,可是萬一有這樣的需求,仍是能夠解除限制的。
在啓動node程序的時候,能夠傳遞兩個參數來調整內存限制的大小。git

node --max-nex-space-size=1024 app.js // 單位爲KB
node --max-old-space-size=2000 app.js // 單位爲MB

這兩條命令分別對應Node內存堆中的「新生代」和「老生代」github

不受內存限制的特例

在Node中,使用Buffer能夠讀取超過V8內存限制的大文件。緣由是Buffer對象不一樣於其餘對象,它不通過V8的內存分配機制。這在於Node並不一樣於瀏覽器的應用場景。在瀏覽器中,JavaScript直接處理字符串便可知足絕大多數的業務需求,而Node則須要處理網絡流和文件I/O流,操做字符串遠遠不能知足傳輸的性能需求。算法

內存的分配

一切JavaScript對象都用堆來存儲後端

當咱們在代碼中聲明變量並賦值時,所使用對象的內存就分配在堆中。若是已申請的對空閒內存不夠分配新的對象,講繼續申請堆內存,直到堆的大小超過V8的限制爲止。瀏覽器

V8的堆示意圖

V8的垃圾回收機制

分代式垃圾回收

V8的垃圾回收策略主要基於「分代式垃圾回收機制」,基於這個機制,V8把內存分爲「新生代(New Space)」和 「老生代 (Old Space)」。
新生代中的對象爲存活時間較短的對象,老生代中的對象爲存活時間較長或常駐內存的對象。
前面說起到的--max-old-space-size命令就是設置老生代內存空間的最大值,而--max-new-space-size命令則能夠設置新生代內存空間的大小。

V8的分代示意圖

爲何要分紅新老兩代?

垃圾回收算法有不少種,可是並無一種是勝任全部的場景,在實際的應用中,須要根據對象的生存週期長短不一,而使用不一樣的算法,已達到最好的效果。在V8中,按對象的存活時間將內存的垃圾回收進行不一樣的分代,而後分別對不一樣的內存施以更高效的算法。

新生代中的垃圾回收

在新生代中,主要經過Scavenge算法進行垃圾回收。

Scavenge

在Scavenge算法中,它將堆內存一分爲二,每一部分空間稱爲semispace。在這兩個semispace空間中,只有一個處於使用中,另一個處於閒置狀態。處於使用狀態的semispace稱爲From空間,處於閒置狀態的semispace稱爲To空間。當咱們分配對象時,先是從From空間中分配。當開始進行垃圾回收時,會檢查From空間中存活的對象,這些存活的對象會被複制到To空間中,而非存活的對象佔用的空間會被釋放。完成複製後,From空間和To空間角色互換。簡而言之,在垃圾回收的過程當中,就是經過將存活對象在兩個semispace空間之間進行復制。

V8的堆內存示意圖

在新生代中的對象怎樣才能到老生代中?

在新生代存活週期長的對象會被移動到老生代中,主要符合兩個條件中的一個:

1. 對象是否經歷過Scavenge回收。
對象從From空間中複製到To空間時,會檢查它的內存地址來判斷這個對象是否已經經歷過一次Scavenge回收,若是已經經歷過了,則將該對象從From空間中複製到老生代空間中。

2. To空間的內存佔比超過25%限制。
當對象從From空間複製到To空間時,若是To空間已經使用超過25%,則這個對象直接複製到老生代中。這麼作的緣由在於此次Scavenge回收完成後,這個To空間會變成From空間,接下來的內存分配將在這個空間中進行。若是佔比太高,會影響後續的內存分配。

老生代中的垃圾回收

對於老生代的對象,因爲存活對象佔比較大比重,使用Scavenge算法顯然不科學。一來複制的對象太多會致使效率問題,二來須要浪費多一倍的空間。因此,V8在老生代中主要採用「Mark-Sweep」算法與「Mark-Compact」算法相結合的方式進行垃圾回收。

Mark-Sweep

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

Mark-Sweep在老生代空間中標記後的示意圖

可是Mark-Sweep有一個很嚴重的問題,就是進行一次標記清除回收以後,內存會變得碎片化。若是須要分配一個大對象,這時候就沒法完成分配了。這時候就該Mark-Compact出場了。

Mark-Compact

Mark-Compact是標記整理的意思,是在Mark-Sweep基礎上演變而來。Mark-Compact在標記存活對象以後,在整理過程當中,將活着的對象往一端移動,移動完成後,直接清理掉邊界外的內存。

Mark-Compact完成標記並移動存活對象後的示意圖

Incremental Marking

鑑於Node單線程的特性,V8每次垃圾回收的時候,都須要將應用邏輯暫停下來,待執行完垃圾回收後再恢復應用邏輯,被稱爲「全停頓」。在分代垃圾回收中,一次小垃圾回收只收集新生代,且存活對象也相對較少,即便全停頓也沒有多大的影響。可是在老生代中,存活對象較多,垃圾回收的標記、清理、整理都須要長時間的停頓,這樣會嚴重影響到系統的性能。
因此「增量標記 (Incrememtal Marking)」被提出來。它從標記階段入手,將本來要一口氣停頓完成的動做改成增量標記,拆分爲許多小「步進」,每作完一「步進」就讓JavaScript應用邏輯執行一小會,垃圾回收與應用邏輯這樣交替執行直到標記階段完成。

內存泄露排查的工具

node-heapdump

它容許對V8堆內存抓取快照,用於過後分析。
在程序中引入

var heapdump = require("node-heapdump");

以後能夠經過向服務器發送SIGUSR2信號,讓node-heapdump抓拍一份堆內存的快照:

$ kill -USR2 <pid>

這份抓拍的快照會默認存放在文件目錄下,這是一份大JSON文件,能夠經過Chrome的開發者工具打開查看。

Chrome Profile

node-memwatch

須要注意,node-memwatch只是支持到node v0.12.x爲止,當使用更高的版本的時候,就會安裝不上,這時候可使用node-watch-next 替代,一摸同樣的API。

不一樣於node-heapdump,它提供了兩個事件監聽器,用來提供內存泄露的以及垃圾回收的信息:

  1. stats事件:每次進行全堆回收時,會觸發改時間,傳遞內存的統計信息

  2. leak事件:通過五次垃圾回收以後,內存仍沒有被釋放的對象,會觸發leak事件,傳遞相關的信息。

node-profiler

node-profiler 是 alinode團隊出品的一個與node-heapdump相似的抓取內存堆快照的工具,不一樣的是,node-profiler的實現不同,使用起來更便捷。附上他們的教程:如何使用Node Profiler

alinode

alinode官方如似說:

alinode 是阿里雲出品的 Node.js 應用服務解決方案,是一套基於社區 Node 改進的運行時環境和服務平臺。在社區的基礎上咱們內建了強大的支持功能,幫助開發者迅速洞見性能細節,快速定位疑難雜症,直探問題根源。

以上內容參考自

A tour of V8: Garbage Collection
V8 之旅: 垃圾回收器
《深刻淺出Node.js》

相關文章
相關標籤/搜索