1. 程式人生 > >東哥學Node的故事——內存管理

東哥學Node的故事——內存管理

調用 我們 引用 其余 log文件 滿了 終端 服務器端 垃圾回收算法

前言

東哥是一個平凡的前端攻城獅,北郵網研院研二在讀,剛接觸Node不久,心裏充滿了對Node的好奇和崇拜,只聽噗通一聲,掉入了Node的坑。。。

於是東哥開始瘋狂地看Node相關的書籍,這不,就學到了Node.js內存管理這一章。

他讀到:“對於那些短時間執行的場景,比如網頁應用、命令行工具,內存的管理似乎沒有太大的必要。因為運行時間短,隨著進程的退出,內存得到釋放,幾乎沒有內存泄露,即使存在內存使用過多的情況,也只會影響到終端用戶。所以,我們在使用JavaScript進行前端開發的過程中,很少會考慮內存管理的問題”。

東哥覺得很有道理,產生了共鳴:“是啊,幹了這麽久前端,寫了這麽多網頁,還真沒考慮過內存問題!”

順便提一句,東哥還是一個算法狂人,曾經使用Java這門走遍天下的語言刷遍了各大平臺(Leetcode、劍指offer、牛客網。。。)的算法題,可謂十分強勢!

東哥很聰明,心想:既然Node.js是一個針對服務器端開發的平臺,也應該和Java一樣存在一些諸如內存泄露、內存分配優化等問題吧。

他讀到:“隨著Node.js在服務器端的廣泛應用,其他語言在內存管理上存在的問題在JavaScript中也暴露了出來。”

心想:“有道理,讓俺一睹其究竟!”

V8垃圾回收機制與內存限制

Node與V8

2009年,Node的創始人Ryan Dahl選擇了V8來作為Node的JavaScript腳本引擎,在第三次瀏覽器大戰中,Google的Chrome瀏覽器憑借V8的優異性能成為焦點。

V8內存限制

在一般的後端開發語言中,在基本的內存使用上沒有什麽限制,然而在Node中通過JavaScript使用內存是就會發現只能使用部分內存(64位系統下約為1.4GB,32位系統下約為0.7GB)。

最近,東哥剛入手了一臺32GB內存的服務器用於大數據分析處理,有一天,他試圖將一個2GB的文件讀入內存中進行字符串分析處理,這豈不是小菜一碟?可是。。。東哥失敗了。。。

造成這個問題的主要原因在於Node是基於V8構建的,所以在Node中使用的JavaScript對象基本上都是通過V8自己的方式進行管理分配的。V8的這套內存管理機制對於瀏覽器端使用起來可謂綽綽有余,但在服務器端卻大大限制了開發者隨心所欲地使用大內存的想法。

V8對象分配

在V8中,所有的JavaScript對象都是通過堆來進行分配的。V8堆示意圖如下:

技術分享

可以使用process.memoryUsage()來查看內存使用量:

技術分享

其中,rss是resident set size的縮寫,即進程的常駐內存部分。進程的內存總共有幾部分,一部分是rss,其余部分在交換區(swap)或者文件系統(filesystem)中;除了rss外,heapTotal和heapUsed對應V8堆內存的信息,heapTotal是堆中總共申請的內存空間,heapUsed是目前堆中使用的內存空間。

除此以外,還可以使用os模塊的totalmem()和freemem()兩個方法查看操作系統的內存使用情況,它們分別返回的是系統的總內存和閑置內存,以字節為單位:

技術分享

可見,我這臺屌絲機系統總內存為4GB,當前閑置內存大致為2.8GB。

東哥在熟悉了查看內存信息的方法後一直很納悶,V8為什麽要限制堆的大小呢,這樣做是不是會讓Node內存使用性能變得很低下?

表層原因是因為V8最初是為瀏覽器而設計的,不太可能有用到大量內存的情況。而深層原因是V8的垃圾回收機制的限制。

當然,我們也可以自行配置內存使用空間,因為V8也給開發者提供了選項讓我們使用更多的內存。示例如下:

node --max-old-space-size=1700 test.js //單位為MB
node --max-new-space-size=1700 test.js //單位為KB  

V8垃圾回收機制

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

技術分享

涉及到的垃圾回收算法算法主要有三種:

技術分享

關於算法的細節,由於時間有限,就不在這裏詳細描述了,大家可以對比著看看各種算法的特點。

如何查看垃圾回收日誌?

查看垃圾回收日誌的方式主要是在啟動時添加--trace_gc參數。將會在gc.log文件中得到所有的垃圾回收信息:

技術分享

gc.log文件大概長這個樣:

技術分享

通過查看gc.log文件,我們可以找出回收哪些階段比較耗時,觸發的原因是什麽。

另外,通過在Node啟動時使用--prof參數,可以得到V8執行時的性能分析數據,其中包含了垃圾回收執行時占用的時間。我在本地寫了一個test.js文件:

for(var i=0;i<1000000;i++){
    var a = {};
}

執行如下命令:

技術分享

會在目錄下得到一個v8.log日誌文件,長這樣:

技術分享

顯然,該文件不具備可讀性。。。所幸,V8提供了linux-tick-processor工具用於統計日誌信息。我們執行:

技術分享

就能得到統計結果,大致如下:

技術分享

統計內容較多。其中,垃圾回收部分如下:

技術分享

由於不斷分配對象,垃圾回收所占的時間為5.4%.按此比例,時間循環執行1000毫秒的過程中要給出54毫秒用於垃圾回收。

高效使用內存

作用域(鏈)

在JavaScript中能形成作用域的有函數調用、with語句以及全局作用域。以如下代碼為例:

var foo = function(){
   var local = {}; 
};

foo()函數在每次被調用時會創建對應的作用域,函數執行結束後,該作用域將會銷毀。同時作用域中申明的局部變量隨著作用域的銷毀而銷毀,局部變量local失效,其引用的對象將會在下次垃圾回收時被釋放。

而作用域鏈是指JavaScript執行過程中變量的查找會沿著一層一層的作用域形成的鏈進行,一直到全局作用域。

所以,主動釋放變量可以合理地利用作用域(鏈)原理,讓我們高效地使用內存。

閉包

閉包大家再熟悉不過了,在JavaScript中,實現外部作用域訪問內部作用域中的變量的方法叫做閉包。而閉包的存在,會使得局部變量常駐內存,不能被及時釋放回收。

所以,閉包要慎用。即使使用,也要在適當的時候主動釋放局部變量。

內存泄露

內存泄露的原因主要有如下幾個:

1、緩存

2、隊列消費不及時

3、作用域未及時釋放

所以,預防措施主要有如下幾個:

1、慎將內存當做緩存使用

2、關註隊列狀態

3、及時釋放作用域中的對象和變量

內存泄露排查

推薦幾個排查工具,可通過npm安裝使用:

1、v8-profiler

2、node-heapdump

3、node-mtrace

4、dtrace

5、node-memwatch

東哥學Node的故事——內存管理