memwatch-next 是一个用来监测 Node.js 的内存泄漏和堆信息比较的模块,以下简称 memwatch。
下面我们以一段事件监听器导致内存泄漏的代码为例,讲解如何使用 memwatch。
测试代码:
app.js
let count = 1
const memwatch = require('memwatch-next')
memwatch.on('stats', (stats) => {
console.log(count++, stats)
})
memwatch.on('leak', (info) => {
console.log('---')
console.log(info)
console.log('---')
})
const http = require('http')
const server = http.createServer((req, res) => {
for (let i = 0; i < 10000; i++) {
server.on('request', function leakEventCallback() {})
}
res.end('Hello World')
global.gc()
}).listen(3000)
每一个请求到来时,给 server 注册 10000 个 request 事件的监听函数(大量的事件监听函数存储到内存中,造成了内存泄漏),然后手动触发一次 GC。
运行该程序:
$ node --expose-gc app.js
注意:这里添加 --expose-gc 参数启动,这样我们才可以在程序中手动触发 GC。
memwatch 可以监听两个事件:
-
stats:GC 事件,每执行一次 GC,触发该函数,打印 heap 相关信息。如下:
{ num_full_gc: 1,// 完整的垃圾回收次数 num_inc_gc: 1,// 增长的垃圾回收次数 heap_compactions: 1,// 内存压缩次数 usage_trend: 0,// 使用趋势 estimated_base: 5350136,// 预期基数 current_base: 5350136,// 当前基数 min: 0,// 最小值 max: 0// 最大值 }
-
leak:内存泄露事件,触发该事件的条件是:连续 5 次 GC 后内存都是增长的。打印如下:
{ growth: 4051464, reason: 'heap growth over 5 consecutive GCs (2s) - -2147483648 bytes/hr' }
运行:
$ ab -c 1 -n 5 http://localhost:3000/
输出:
(node:20989) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 request listeners added. Use emitter.setMaxListeners() to increase limit
1 { num_full_gc: 1,
num_inc_gc: 1,
heap_compactions: 1,
usage_trend: 0,
estimated_base: 5720064,
current_base: 5720064,
min: 0,
max: 0 }
2 { num_full_gc: 2,
num_inc_gc: 1,
heap_compactions: 2,
usage_trend: 0,
estimated_base: 7073824,
current_base: 7073824,
min: 0,
max: 0 }
3 { num_full_gc: 3,
num_inc_gc: 1,
heap_compactions: 3,
usage_trend: 0,
estimated_base: 7826368,
current_base: 7826368,
min: 7826368,
max: 7826368 }
4 { num_full_gc: 4,
num_inc_gc: 1,
heap_compactions: 4,
usage_trend: 0,
estimated_base: 8964784,
current_base: 8964784,
min: 7826368,
max: 8964784 }
---
{ growth: 3820272,
reason: 'heap growth over 5 consecutive GCs (0s) - -2147483648 bytes/hr' }
---
5 { num_full_gc: 5,
num_inc_gc: 1,
heap_compactions: 5,
usage_trend: 0,
estimated_base: 9540336,
current_base: 9540336,
min: 7826368,
max: 9540336 }
可以看出:Node.js 已经警告我们事件监听器超过了 11 个,可能造成内存泄露。连续 5 次内存增长触发 leak 事件打印出增长了多少内存(bytes)和预估每小时增长多少 bytes。
memwatch 有一个 HeapDiff 函数,用来对比并计算出两次堆快照的差异。修改测试代码如下:
const memwatch = require('memwatch-next')
const http = require('http')
const server = http.createServer((req, res) => {
for (let i = 0; i < 10000; i++) {
server.on('request', function leakEventCallback() {})
}
res.end('Hello World')
global.gc()
}).listen(3000)
const hd = new memwatch.HeapDiff()
memwatch.on('leak', (info) => {
const diff = hd.end()
console.dir(diff, { depth: 10 })
})
运行这段代码并执行同样的 ab 命令,打印如下:
{ before: { nodes: 35727, size_bytes: 4725128, size: '4.51 mb' },
after: { nodes: 87329, size_bytes: 8929792, size: '8.52 mb' },
change:
{ size_bytes: 4204664,
size: '4.01 mb',
freed_nodes: 862,
allocated_nodes: 52464,
details:
[ ...
{ what: 'Array',
size_bytes: 530200,
size: '517.77 kb',
'+': 1023,
'-': 510 },
{ what: 'Closure',
size_bytes: 3599856,
size: '3.43 mb',
'+': 50001,
'-': 3 },
...
]
}
}
可以看出:内存由 4.51mb 涨到了 8.52mb,其中 Closure 和 Array 涨了绝大部分,而我们知道注册事件监听函数的本质就是将事件函数(Closure)push 到相应的数组(Array)里。
memwatch 结合 heapdump 使用才能发挥更好的作用。通常用 memwatch 监测到发生内存泄漏,用 heapdump 导出多份堆快照,然后用 Chrome DevTools 分析比较,定位内存泄漏元凶。
修改代码如下:
const memwatch = require('memwatch-next')
const heapdump = require('heapdump')
const http = require('http')
const server = http.createServer((req, res) => {
for (let i = 0; i < 10000; i++) {
server.on('request', function leakEventCallback() {})
}
res.end('Hello World')
global.gc()
}).listen(3000)
dump()
memwatch.on('leak', () => {
dump()
})
function dump() {
const filename = `${__dirname}/heapdump-${process.pid}-${Date.now()}.heapsnapshot`
heapdump.writeSnapshot(filename, () => {
console.log(`${filename} dump completed.`)
})
}
代码含义:程序启动后先执行一次 heap dump,当触发 leak 事件时再执行一次 heap dump。运行这段代码并执行同样的 ab 命令,生成两个 heapsnapshot 文件:
heapdump-21126-1519545957879.heapsnapshot
heapdump-21126-1519545975702.heapsnapshot
用 Chrome DevTools 加载这两个 heapsnapshot 文件,选择 comparison 比较视图,如下所示:
可以看出:增加了 5 万个 leakEventCallback 函数,选中任意一个,可以从 Retainers 中看到更详细的信息,如 GC path 和所在的文件等信息。
上一节:2.2 heapdump