垃圾回收機制
nodejs在執(zhí)行JavaScript時,內(nèi)存受到v8限制,64位約為1.4g,32位0.7g
所有js對象是通過堆分配,查看process.memoryUsage()
限制內(nèi)存原因:垃圾回收時,js線程會暫停執(zhí)行(避免JS應(yīng)用邏輯與垃圾回收器看到的不一樣),大量的堆內(nèi)存回收嚴重影響性能
-
v8內(nèi)存整體包含新生代和老生代
// 調(diào)整內(nèi)存限制的大小 node --max-old-space-size=1700 test.js // 單位為MB node --max-new-space-size=1024 test.js // 單位為KB 在V8初始化時生效,一旦生效不能動態(tài)變化
新生代
- 由兩個reserved_semispace_size_(32位16mb,62位32mb)構(gòu)成
- 通過Scavenge算法進行回收,具體實現(xiàn)采用Cheney算法
Cheney算法采用復(fù)制方式實現(xiàn)垃圾回收,將堆內(nèi)存分成2塊,一個使用(From),一個空閑(To).分配對象在From空間。開始垃圾回收時,檢查From里的存活對象,并將它們復(fù)制到To,非存活對象占用的空間會被釋放,然后From、to角色對換。將存活對象在兩個空間之間復(fù)制
優(yōu)點是時間短、缺點是只能使用一半堆內(nèi)存。新生代對象生命周期短,適合此算法
當對象經(jīng)過多次復(fù)制依然存活,就會晉升到老生代。對像晉升的條件,是否經(jīng)歷過Scavenge回收,To空間內(nèi)存占用比超過限制
老生代
- 在64未系統(tǒng)下為1400 MB,在32為700 MB
- 使用Mark-Sweep和Mark-Compact進行垃圾回收
Mark-Sweep 標記清除,先遍歷堆的對象,標記活的對象,之后清除沒標記的對象。死對象在老生代只占少部分,所以高效
Mark-Compact 整理內(nèi)存,將Mark-Sweep清理后散開的對象移動到一段。
v8主要使用Mark-Sweep,在空間不足以對新晉升對象分配時才用Mark-Compact
增量標記(incremental marking)
- 降低老生代的全堆垃圾回收帶來的時間停頓
- 從標記階段入手,拆分為許多小步進,與應(yīng)用邏輯交替運行
- 垃圾回收最大停頓時間降為原來的1/6
垃圾回收是影響性能的因素之一,要盡量減少垃圾回收,尤其全堆垃圾回收
查看垃圾回收日志
- 在啟動時添加--trace_gc
啟動時使用--prof,可以得到v8性能分析數(shù)據(jù),包含垃圾回收占用的時間,需要使用工具讀取,在Node源碼的deps/v8/tools,linux-tick-processor
高效使用內(nèi)存
作用域
- js中能形成作用域的有函數(shù)調(diào)用、with和全局作用域
例如,在函數(shù)調(diào)用時,會創(chuàng)建對應(yīng)的作用域,在執(zhí)行結(jié)束后銷毀,并且在該作用域申明的局部變量也會被銷毀
- 標識符查找(即變量名) 先查找當前作用域,再向上級作用域,一直到全局作用域
- 變量主動釋放 全局變量要直到進程退出才釋放,導(dǎo)致引用對象常駐老生代,可以用delete刪除或者賦undefined、null(delete刪除對象的屬性可能干擾v8,所以賦值更好)
閉包
- 外部作用域訪問內(nèi)部作用域的方法,得益于高階函數(shù)特性
var foo = function() {
var bar = function() {
var local = "局部變量";
return function() {
return local;
};
};
var baz = bar();
console.log(baz());
};
bar()返回一個匿名函數(shù),一旦 有變量引用它,它的作用域?qū)⒉粫尫?,直到?jīng)]有引用
把閉包賦值給一個不可控的對象時,會導(dǎo)致內(nèi)存泄漏。使用完,將變量賦其他值或置空
查看內(nèi)存使用情況
- 查看進程內(nèi)存占用 process.memoryUsage(),其中rss為進程的常駐內(nèi)存(node所占的內(nèi)存),heapTotal、heapUsed為堆內(nèi)存使用情況
- os.totalmem(),os.freemem() 查看系統(tǒng)內(nèi)存
Node使用的內(nèi)存不是都通過v8分配,還有堆外內(nèi)存,用于處理網(wǎng)絡(luò)流、I/O流
內(nèi)存泄漏
造成的原因:緩存、隊列消費不及時、作用域未釋放
緩存
- 限制內(nèi)存當緩存,要限制好大小,做好釋放
- 進程之間不能共享內(nèi)存,所以用內(nèi)存做緩存也是
為了加速模塊引入,模塊會在編譯后緩存,由于通過exports導(dǎo)出(閉包),作用域不會釋放,常駐老生代。要注意內(nèi)存泄漏
var leakArray = [];
exports.leak = function() {
leakArray.push("leak" + Math.random());
};
//局部變量leakArray不停增加內(nèi)存占用,且不會釋放,如果必須如此設(shè)計,要提供釋放接口
推薦使用進程外緩存,<a target='_blank'>Redis</a>、<a target='_blank'>Memcached</a>
隊列狀態(tài)
- 在生產(chǎn)者和消費者中間
- 監(jiān)控隊列的長度,超過長度就拒絕
- 任意的異步調(diào)用應(yīng)該包含超時機制
內(nèi)存泄漏排查
node-heapdump
- 安裝 npm install heapdump
- 在開頭引入 var heapdump = require('heapdump');
- 發(fā)送命令kill -USR2 <pid>,heapdump會抓拍一份堆內(nèi)存快照,文件為heapdump-<sec>.<usec>.heapsnapshot格式,是json文件
node-memwatch
var memwatch = require('memwatch');
memwatch.on('leak', function(info) {
console.log('leak:');
console.log(info);
});
memwatch.on('stats', function(stats) {
console.log('stats:') console.log(stats);
});
在進程使用node-memwatch后,每次全堆垃圾回收,會觸發(fā)stats事件,該事件會傳遞內(nèi)存的統(tǒng)計信息
stats: {
num_full_gc: 4, // 第幾次全堆垃圾回收
num_inc_gc: 23, // 第幾次增量垃圾回收
heap_compactions: 4, // 第幾次對老生代整理
usage_trend: 0, // 使用趨勢
estimated_base: 7152944, // 預(yù)估基數(shù)
current_base: 7152944, // 當前基數(shù)
min: 6720776, // 最小
max: 7152944 //最大
}
如果經(jīng)過連續(xù)的5次垃圾回收后,內(nèi)存仍沒有被釋放,意味有內(nèi)存泄漏,node-memwatch會觸發(fā)leak事件
leak: 8 {
start: Mon Oct 07 2013 13: 46: 27 GMT + 0800(CST),
end: Mon Oct 07 2013 13: 54: 40 GMT + 0800(CST),
growth: 6222576,
reason: 'heap growth over 5 consecutive GCs (8m 13s) - 43.33 mb/hr'
}
//顯示內(nèi)存增長了多了
使用node-memwatch的抓取快照和比較快照,能將內(nèi)存泄漏定位到v8的堆上
var memwatch = require('memwatch');
var leakArray = [];
var leak = function() {
leakArray.push("leak" + Math.random());
};
// Take first snapshot
var hd = new memwatch.HeapDiff();
for (var i = 0; i < 10000; i++) {
leak();
}
// Take the second snapshot and compute the diff
var diff = hd.end();
console.log(JSON.stringify(diff, null, 2));
{
"before": {
"nodes": 11719,
"time": "2013-10-07T06:32:07.000Z",
"size_bytes": 1493304,
"size": "1.42 mb"
},
"after": {
"nodes": 31618,
"time": "2013-10-07T06:32:07.000Z",
"size_bytes": 2684864,
"size": "2.56 mb"
},
"change": {
"size_bytes": 1191560,
"size": "1.14 mb",
"freed_nodes": 129, //釋放的節(jié)點數(shù)
"allocated_nodes": 20028,//分配的節(jié)點數(shù)
"details": [{
"what": "Array",
"size_bytes": 323720,
"size": "316.13 kb",
"+": 15,
"-": 65
}, {
"-": 28
}, {
"what": "String",
"size_bytes": 879424,
"what": "Code",
"size_bytes": -10944,
"size": "-10.69 kb",
"+": 8,
"size": "858.81 kb",
"+": 20001, //大量的string未被回收
"-": 1
}]
}
}
大內(nèi)存應(yīng)用
- 使用stream模塊處理大文件,fs的createReadStream(),createWriteStream()
var reader = fs.createReadStream('in.txt');
var writer = fs.createWriteStream('out.txt');
reader.on('data', function(chunk) {
writer.write(chunk);
});
reader.on('end', function() {
writer.end();
});
//管道方法pipe(),封裝了data事件和寫入
var reader = fs.createReadStream('in.txt');
var writer = fs.createWriteStream('out.txt');
reader.pipe(writer);
在不需要進行字符串操作時,可以不借助v8,使用Buffer操作,這樣不會受到v8的內(nèi)存限制