東哥學Node的故事——內存管理
前言
東哥是一個平凡的前端攻城獅,北郵網研院研二在讀,剛接觸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的故事——內存管理