之前一次偶然機會發現,react 在server渲染時,當NODE_ENV != production時,會導致內存洩漏。具體issues: https://github.com/facebook/react/issues/7406 。隨著node,react同構等技術地廣泛運用,node端內存洩漏等問題應該引起我們的重視。為什麼node容易出現內存洩漏以及出現之後應該如何排查,下面通過一個簡單的介紹以及例子來說明。
首先,node是基於v8引擎基礎上,其內存管理方式與v8一致。下面簡單介紹v8的相關內存特效。
V8內存限制
node基於V8構建,通過V8的方式進行分配跟管理js對象。 V8對內存的使用有限制(老生代內存64位系統下約為1.4G,32位系統下約為0.7G,新生代內存64位系統下約為32MB,32系統下約為16MB)。在這樣的限制下,將導致無法操作大內存對象。如果不小心觸碰這個界限,就會造成進程退出。
原因:V8在執行垃圾回收時會阻塞JavaScript應用邏輯,直到垃圾回收結束再重新執行JavaScript應用邏輯,這種行為被稱為“全停頓”(stop-the-world)。若V8的堆內存為1.5GB,V8做一次小的垃圾回收需要50ms以上,做一次非增量式的垃圾回收甚至要1秒以上。
通過node --max-old-space-size=xxx(單位MB) , node --max-new-space-size=xxx(單位KB) 設置新生代內存以及老生代內存來破解默認的內存限制。
V8的堆構成
V8的堆其實並不只是由老生代和新生代兩部分構成,可以將堆分為幾個不同的區域:
GC回收類型
增量式GC
表示垃圾回收器在掃描內存空間時是否收集(增加)垃圾並在掃描週期結束時清空垃圾。
非增量式GC
使用非增量式垃圾收集器時,一收集到垃圾即將其清空。
垃圾回收器只會針對新生代內存區、老生代指針區以及老生代數據區進行垃圾回收。對象首先進入佔用空間較少的新生代內存。大部分對象會很快失效,非增量GC直接回收這些少量內存。假如有些對像一段時間內不能被回收,則進去老生代內存區。這個區域則執行不頻繁的增量GC,且耗時較長。
那什麼時候才會導致內存洩漏的發生呢?
內存洩漏的途徑
Node的內存構成主要是通過V8進行分配的部分和Node自行分配的部分。受V8的垃圾回收限制的主要是V8的堆內存。造成內存洩漏的主要原因:1,緩存;2,隊列消費不及時;3,作用域未釋放
內存洩漏分析
查看V8內存使用情況(單位byte)
process.memoryUsage(); { ress: 47038464, heapTotal: 34264656, heapUsed: 2052866 }ress:進程的常駐內存部分
heapTotal,heapUsed:V8堆內存信息
查看系統內存使用情況(單位byte)
os.totalmem()
os.freemem()
返回系統總內存以及閒置內存
查看垃圾回收日誌
node --trace_gc -e "var a = []; for( var i = 0; i < 1000000; i++ ) { a.push(new Array(100)); }" >> gc.log //輸出垃圾回收日誌
node --prof //輸出node執行時性能日誌。 使用windows-tick.processor查看。
分析監控工具
v8-profiler 對v8堆內存抓取快照和對cpu進行分析
node-heapdump 對v8堆內存抓取快照
node-mtrace 分析堆棧使用
node-memwatch 監聽垃圾回收情況
node-memwatch
memwatch.on('stats',function(info){ console.log(info)})memwatch.on('leak',function(info){ console.log(info)})stats事件:每次進行全堆垃圾回收時,將觸發一次stats事件。這個事件將會傳遞內存統計信息。
{"num_full_gc": 17, //第幾次全棧垃圾回收"num_inc_gc": 8, //第幾次增量垃圾回收"heap_compactions": 8, //第幾次對老生代進行整理"estimated_base": 2592568, //預估基數"current_base": 2592568, //當前基數"min": 2499912, //最小"max": 2592568, //最大"usage_trend": 0 //使用趨勢}觀察num_full_gc和num_inc_gc反映垃圾回收情況。
leak事件:如果經過連續5次垃圾回收後,內存仍然沒有被釋放,意味著內存洩漏的發生。這個時候會觸發一個leak事件。
{ start: Fri, 29 Jun 2012 14:12:13 GMT,end: Fri, 29 Jun 2012 14:12:33 GMT,growth: 67984,reason: 'heap growth over 5 consecutive GCs (20s) - 11.67 mb/hr'}Heap Diffing 堆內存比較排查內存溢出代碼。
下面,我們通過一個例子來演示如何排查定位內存洩漏:
首先我們創建一個導致內存洩漏的例子:
//app.jsvar app = require('express')();var http = require('http').Server(app);var heapdump = require('heapdump');var leakobjs = [];function LeakClass(){ this.x = 1;}app.get('/', function(req, res){ console.log('get /'); for(var i = 0; i < 1000; i++){ leakobjs.push(new LeakClass()); } res.send('<h1>Hello world</h1>');});setInterval(function(){ heapdump.writeSnapshot('./' + Date.now() + '.heapsnapshot');}, 3000);http.listen(3000, function(){ console.log('listening on port 3000');});這裡我們通過設置一個不斷增加且不回被回收的數組,來模擬內存洩漏。
通過使用heap-dump模塊來定時紀錄內存快照,並通過chrome開發者工具profiles來導入快照,對比分析。
我們可以看到,在瀏覽器訪問localhost:3000 ,並多次刷新後,快照的大小一直在增長,且即使不請求,也沒有減小,說明已經發生了洩漏。
接著我們通過過chrome開發者工具profiles, 導入快照。通過設置comparison,對比初始快照,發送請求,平穩,再發送請求這3個階段的內存快照。可以發現右側new中LeakClass一直增加。在delta中始終為正數,說明並沒有被回收。
小結
針對內存洩漏可以採用植入memwatch,或者定時上報process.memoryUsage內存使用率到monitor,並設置告警閥值進行監控。
當發現內存洩漏問題時,若允許情況下,可以在本地運行node-heapdump,使用定時生成內存快照。並把快照通過chrome Profiles分析洩漏原因。若無法本地調試,在測試服務器上使用v8-profiler輸出內存快照比較分析json(需要代碼侵入)。
需要考慮在什麼情況下開啟memwatch/heapdump。考慮heapdump的頻度以免耗盡了CPU。 也可以考慮其他的方式來檢測內存的增長,比如直接監控process.memoryUsage()。
當心誤判,短暫的內存使用峰值表現得很像是內存洩漏。如果你的app突然要佔用大量的CPU和內存,處理時間可能會跨越數個垃圾回收週期,那樣的話memwatch很有可能將之誤判為內存洩漏。但是,這種情況下,一旦你的app使用完這些資源,內存消耗就會降回正常的水平。所以需要注意的是持續報告的內存洩漏,而可以忽略一兩次突發的警報。