node对内存泄露十分敏感,这个不同于浏览器,一旦内存堆积,垃圾回收无法释放,会耗费大量时间进行对象扫描,应用会变慢,直到进程崩溃。
v8垃圾回收机制
v8的垃圾回收策略叫分代式垃圾回收机制。怎么说呢?
这个分代,就是把内存分成两部分:新生代(new space)和老生代(old space)。
新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。
新生代的垃圾回收——Scavenge
Scavenge就是拾荒的意思,它将堆内存一分为二,每一部分称为半空间semispace。
在这两个semispace空间中,只有一个处于使用中,另外一个处于闲置状态。处于使用状态的semispace称为From空间,处于闲置状态的semispace称为To空间。
当我们分配对象时,先是从From空间中分配。当From空间一旦满了就会进行垃圾回收,这时会检查From空间中存活的对象,把这些存活的对象复制到To空间中,而垃圾对象占用的空间会被释放掉。
完成复制后,From空间和To空间的角色会发生互换,To变From,From变To,然后重复前面的过程。
这种算法比较简单,适合比较活跃的对象,但如果某对象经历过一次Scavenge回收,下一次又继续使用,那说明这个对象存活时间较长,会将该对象复制到老生代空间里去。
老生代的垃圾回收——Mark-Sweep和Mark-Compact
Mark-Sweep就是标记清除,步骤是:
垃圾回收时,先遍历堆内存的所有对象,标记存活的对象,然后清除掉未标记的对象。
但清除完成后,内存会有很多碎片,我们需要整理一下,有点像磁盘工具,这就是Mark-Compact。
既然内存泄露问题如此重要,那如何防范呢?
监听gc垃圾回收情况
我们可以使用memwatch这个工具来监听内存的垃圾回收情况。
npm install memwatch-next -S
下面做一个测试。
在arr这个数组中无限注入回调函数,导致arr这个变量始终无法释放,这个对象会常驻于老生代空间中,运行几次后必然会导致溢出现象。
let http = require('http')
let memwatch = require('memwatch-next')
let count = 1
let arr = []
memwatch.gc()//测试:手动调用垃圾回收
memwatch.on('leak',(info)=>{
console.log(info) //5次以上
})
memwatch.on('stats',(stats)=>{
//每执行一次 gc,触发该事件,并打印内存相关信息。
console.log(count++,'=>',stats)
})
let server = http.createServer((req,res)=>{
//内存泄露事件,触发该事件的条件是:连续5次gc垃圾回收后,内存还是增长的
for(let i=0;i<100000;i++){
arr.push(function(){})
}
res.end('ok')
})
server.listen(3009)
stats事件的打印结果:
{ num_full_gc: 6,//完整的垃圾回收次数
num_inc_gc: 16,//增长的垃圾回收次数
heap_compactions: 6,//内存压缩次数
usage_trend: 0,//使用趋势
estimated_base: 54914256,//预期基数
current_base: 54914256,//当前基数
min: 12826192,
max: 54914256 }
leak事件的打印结果:
{ growth: 67250808,
reason: 'heap growth over 5 consecutive GCs (10s) - -2147483648 bytes/hr'
}
我们可以利用log4j或其他日志工具,将这些堆栈信息输出到日志里以便分析。
堆内存快照
通过memwatch可以知晓内存是否有泄露,那如何定位到具体问题呢?
我们可以用另外一个工具——heapdump。
npm install heapdump -S
这个工具可以抓取堆快照,并保存为json的格式,我们可以将快照文件用chrome浏览器的devtools打开,选择comparison比较视图来定位问题。
let http = require('http')
let memwatch = require('memwatch-next')
let heapdump = require('heapdump')
let count = 1
let arr = []
dump()//无任何操作,拍第一个堆快照,作为参照系
function dump() {
//文件名
const filename = `${__dirname}/heapdump-${process.pid}-${Date.now()}.heapsnapshot`;
heapdump.writeSnapshot(filename, () => {//保存快照
console.log(`${filename} dump completed.`);
});
}
memwatch.on('leak',(info)=>{
console.log(info)
dump()//第二个快照,对比第一个快照
})
memwatch.on('stats',(stats)=>{
console.log(count++,'=>',stats)
})
let server = http.createServer((req,res)=>{
for(let i=0;i<100000;i++){
arr.push(function(){})
}
res.end('ok')
})
server.listen(3009)
得到快照信息:
heapdump-59352-1586653612856.heapsnapshot
heapdump-59352-1586653617974.heapsnapshot
打开控制台,选择Memory,右键profiles,选择load,即可加载本地快照文件: