基於無阻塞、事件驅動創建的Node服務,具備內存消耗低的優勢,很是適合處理海量的網絡請求。node
對於性能敏感的服務器程序,內存管理的好壞、垃圾回收情況是否優良,都會對服務構成影響。而在Node中,這一切都與Node的JavaScript執行引擎V8息息相關。算法
Node是一個構建在Chrome的JavaScript運行時上的平臺。瀏覽器
V8的性能優點使得用JavaScript寫高性能後臺服務程序成爲可能。緩存
Node在JavaScript的執行上直接受益於V8,能夠隨着V8的升級就能享受到更好的性能或新的語言特性,同時也受到V8的一些限制,例如內存限制。bash
在Node中經過JavaScript對使用內存就會發現只能使用部份內存(64位約1.4GB,32位約0.7G)。服務器
V8 的內存限制會致使Node沒法直接操做大內存對象。網絡
形成這個問題的主要緣由在於Node基於V8構建,因此在Node中使用的JavaScript對象基本上都是經過V8本身的方式來進行分配和管理的。閉包
在V8中,全部的JavaScript對象都是經過堆來進行分配的。ide
Node提供了V8中內存使用量的查看方式。函數
$ node
> process.memoryUsage()
{ rss: 22044672,
heapTotal: 9682944,
heapUsed: 5296232, //已申請的堆內存
external: 8860 } //已使用的堆內存
複製代碼
在代碼中聲明變量並賦值時,所使用對象的內存就分配在堆中。若是已申請的堆空閒內存不夠分配新的對象,將繼續申請堆內存,知道堆的大小超過V8的限制爲止。
V8爲什麼要限制堆的大小?
打開限制使用更多的內存:
node --max-old-space-size=1700 test.js //單位爲MB
//或者
node --max-new-space-size=1024 test.js //單位爲KB
複製代碼
V8用到的各類垃圾回收算法:
V8主要的垃圾回收算法
V8的垃圾回收策略主要基於分代式垃圾回收機制。
現代的垃圾回收算法中按對象的存活時間將內存的垃圾回收進行不一樣的分代,再分別對不一樣分代的內存進行更高效的算法。
V8的內存分代
在V8中,主要將內存分爲新生代和老生代兩代。
新生代中的對象存活時間較短;
老生代中的對象存活時間較長或常駐內存的對象;
V8堆的總體大小就是新生代所用內存加上老生代的內存空間。
Scavenge算法
在分代的基礎上,新生代中的對象主要經過Scavenge算法進行垃圾回收。Scavenge的具體實現中,主要採用Cheney算法。
Cheney算法是一種採用複製的方式實現的垃圾回收算法。
在垃圾回收的過程當中,就是經過將存活對象在兩個semisoace空間進行復制。
Scavenge的缺點:只能使用堆內存的一半。
Scavenge是典型的犧牲空間換取時間的算法,因此沒法大規模地應用到全部的垃圾回收中。
當一個對象通過屢次複製依然存活時,它將被認爲是生命週期較長的對象。這種較長生命週期的對象隨後會被移動到老生代中,採用新的算法進行管理。
對象重新生代中移動到老生代中的過程稱爲晉升。
Mark-Sweep & Mark-Compact
Mark-Sweep是標記清除的意思,分爲標記和清除兩個階段。
Mark-Sweep最大的問題是在進行一次標記清除回收後,內存空間會出現不連續的狀態。
Mark-Compact是標記整理的意思,是在Mark-Sweep的基礎上演變而來;
Incremental Marking
爲了不出現JavaScript應用邏輯與垃圾回收器看到不一致的狀況,垃圾回收的3種基本算法都須要將應用邏輯暫停下來,待執行完垃圾回收再回復執行應用邏輯,這種行爲被稱爲「全停頓」(stop-the-world)。
爲了下降全堆垃圾回收帶來的停頓時間,V8先從標記階段入手,將本來要一口氣停頓完成的動做改成增量標記(Incremental Marking)。
查看垃圾回收日誌的方式主要是在啓動時添加--trace_gc參數。在進行垃圾回收時,將會從標準輸出中打印垃圾回收的日誌信息。
經過分析垃圾回收日誌,能夠了解垃圾回收的運行情況,找出垃圾回收的哪些階段比較耗時,觸發的緣由是什麼。
在V8面前,開發者所要具有的責任是如何讓垃圾回收機制更高效地工做。
在JavaScript中能造成做用域的有函數調用、with以及全局做用域。
標識符查找
做用域鏈
變量的主動釋放
若是須要釋放常駐內存的對象,能夠經過delete操做來刪除引用關係。或者將變量從新賦值,讓舊的對象脫離引用關係。
雖然delete操做和從新賦值具備相同的效果,可是在V8中經過delete刪除對象的屬性可能干擾V8的優化,全部經過賦值方式解除引用更好。
在JavaScript中,實現外部做用域訪問內部做用域中的變量的方法叫作閉包(closure)。這得益於高階函數的特性:函數能夠做爲參數或者返回值。
閉包是JavaScript的高級特性,利用它能夠產生不少的巧妙的效果。一旦有變量引用這個中間函數,這個中間函數將不會釋放,一樣也會使原始的做用域不會獲得釋放,做用域中產生的內存佔用也不會獲得釋放。除非再也不引用,纔會逐步釋放。
查看進程的內存佔用
調用process.memoryUsage()能夠看到Node進程的內存佔用狀況。
rss是resident set size的縮寫,即進程的常駐內存部分。進程的內存總共有幾部分,一部分是rss,其他部分在交換區(swap)或者文件系統(filesystem)中。
查看系統的內存佔用
OS模塊中的totalmem()和freemem()用於查看操做系統的內存使用狀況。
分別返回系統的總內存和閒置內存,單位是字節。
$ node
> os.totalmem()
8446971904
> os.freemem()
2469531648
>
複製代碼
堆中的內存用量老是小於進程的常駐內存量,這意味着Node中的內存使用並不是都是經過V8進行分配的。將不是經過V8分配的內存稱爲堆外內存。
堆外內存能夠突破內存限制。
Node對內存泄露十分敏感,一旦線上應用有成千上萬的流量,哪怕是一個字節的內存泄露也會形成堆積,垃圾回收過程將會耗費更多時間進行對象掃描,應用響應緩慢,直到進程內存溢出,應用崩潰。
內存泄露的緣由:
緩存在應用的做用舉足輕重,能夠十分有效地節省資源。緩存的訪問效率要比I/O的效率高,一旦命中緩存,就能夠節省一次I/O的時間。
一旦一個對象被當作緩存來使用,就意味着它將會常駐在老生代中。緩存中存儲的鍵越多,長期存活的對象也就越多,這將致使垃圾回收在進行掃描和整理時,對這些對象作無用功。
JavaScript開發者一般喜歡用對象的鍵值對來緩存東西,但這與嚴格意義上的緩存有區別,嚴格意義上的緩存有着完善的過時策略,而普通對象的鍵值對並無。
一個無心識形成的內存泄露的場景:memoize
memoize的原理是以參數做爲鍵進行緩存,之內存空間換CPU執行時間。
緩存限制策略
爲了解決緩存中的對象永遠沒法釋放的問題,須要加入一種策略來限制緩存的無限增加。
緩存的解決方案
如何使用大量緩存,目前比較好的解決方案是採用進程外的緩存,進程自身不存儲狀態。
外部的緩存軟件有着良好的緩存過時淘汰策略以及自有的內存管理,不影響Node進程的性能。在Node中主要解決兩個問題:
目前,市場上較好的緩存有Redis和Memcached。
在大多數應用場景下,消費的速度遠遠大於生產的速度,內存泄露不易產生。可是一旦消費速度低於生產速度,將會造成堆積。
表層的解決方案是換用消費速度更高的技術。
深度的解決方案是監控隊列的長度。
1.node-heapunp
2.node-memwatch
Node提供了stream模塊用於處理大文件。