Node.js 内存泄漏排查与 V8 机制详解
本文深入解析 Node.js 基于 V8 引擎的内存管理机制,涵盖新生代与老生代 GC 策略及全停顿问题。详细列举缓存、队列积压及作用域未释放等常见泄漏途径,并介绍 node-memwatch、heapdump 等监控工具的使用。通过实例演示如何利用 Chrome DevTools 分析快照,定位并解决内存泄漏问题,提供从监控到排查的完整解决方案。
Node.js 内存泄漏背景与 V8 基础
此前曾偶然发现,在 React 服务端渲染过程中,当 NODE_ENV 不等于 production 时,会引发内存泄漏问题。相关议题可参考:https://github.com/facebook/react/issues/7406 。随着 Node.js 与 React 同构等技术的广泛应用,Node 端内存泄漏等问题已不容忽视。Node.js 为何容易出现内存泄漏,以及发生泄漏后如何进行排查,下文将通过简要介绍与实例进行说明。
Node.js 基于 V8 引擎构建,其内存管理方式与 V8 保持一致。以下简要介绍 V8 的相关内存特性。
V8 内存限制与堆结构解析
V8 内存限制
Node.js 基于 V8 构建,通过 V8 机制来分配和管理 JavaScript 对象。V8 对内存使用存在限制(在 64 位系统下,老生代内存约为 1.4GB,32 位系统下约为 0.7GB;新生代内存 64 位系统下约为 32MB,32 位系统下约为 16MB)。受此限制影响,无法操作超大内存对象。若不慎触及该界限,将导致进程退出。
原因:V8 在执行垃圾回收时会阻塞 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 的堆并非仅由老生代和新生代两部分构成,可将堆划分为以下几个不同区域:
- 新生代内存区:大多数对象在此分配,该区域虽小但垃圾回收特别频繁。
- 老生代指针区:属于老生代,包含大多数可能存在指向其他对象指针的对象,大多数从新生代晋升的对象会被移动至此。
- 老生代数据区:属于老生代,仅保存原始数据对象,这些对象没有指向其他对象的指针。
- 大对象区:存放体积超越其他区大小的对象,每个对象拥有自己的内存,垃圾回收时不会移动大对象。
- 代码区:存放代码对象,即包含 JIT 编译后指令的对象,会被分配在此。这是唯一拥有执行权限的内存区。
- Cell 区、属性 Cell 区、Map 区:存放 Cell、属性 Cell 和 Map,每个区域存放相同大小的元素,结构简单。
V8 垃圾回收机制:增量与非增量
GC 回收类型
增量式 GC
表示垃圾回收器在扫描内存空间时是否收集(增加)垃圾,并在扫描周期结束时清空垃圾。
非增量式 GC
使用非增量式垃圾收集器时,一旦收集到垃圾即将其清空。
垃圾回收器仅针对新生代内存区、老生代指针区以及老生代数据区进行垃圾回收。对象首先进入占用空间较少的新生代内存。大部分对象会很快失效,非增量 GC 直接回收这些少量内存。若某些对象在一段时间内不能被回收,则进入老生代内存区。该区域执行不频繁的增量 GC,且耗时较长。
那么,什么时候才会导致内存泄漏的发生呢?
Node.js 内存泄漏常见途径
内存泄漏的途径
- 内存泄露
- 缓存
- 队列消费不及时
- 作用域未释放
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.js
var 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使用完这些资源,内存消耗就会降回正常的水平。所以需要注意的是持续报告的内存泄漏,而可以忽略一两次突发的警报。
在 NestJS 中使用管道验证数据与守卫控制访问权限
2026-11-05 12:43:41
Node.js后端生成高强度随机字符串的方法
2026-11-04 16:29:54
Node.js环境中怎么使用axios或http模块发送GET请求
2026-11-03 21:57:15
Node.js环境下怎么实现JS反混淆自动化?常用库与CLI工具推荐
2026-11-03 21:15:54
解决JS逆向常见报错:缺少DOM环境导致代码运行失败
2026-11-03 20:41:59
相关标签:
