学习 Node.js 内存管理和垃圾回收机制

本文详细介绍了JavaScript引擎V8的内存管理,包括内存限制、内存指标的查看,以及V8的垃圾回收机制,如Scavenge算法和Mark-Sweep/Mark-Compact算法。此外,还探讨了内存泄漏的原因、分类,如缓存、模块加载和事件监听,提供了内存泄露排查工具如node-heapdump和v8-analytics,并分享了内存泄漏的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JavaScript 的标准 ECMAScript 里没有对 GC 做相关的要求,因此 JavaScript 的 GC 机制完全由引擎决定:

一般存在三种垃圾回收的方法:

  • stop-the-world: 它指的是在执行垃圾回收的过程中,会暂停程序的执行
  • 增量式 GC(incremental),即程序不需要等到垃圾回收完全结束才能重新开始,在垃圾回收的过程中控制权可以根据情况临时交给程序执行
  • 并发式 GC(concurrent),即在垃圾回收的同时不需要停止程序的运行,两者可以同时进行,只有在个别时候需要短暂停下来让垃圾回收器做一些特殊的操作
V8 内存管理
  • 内存限制

受垃圾回收机制的限制(耗时长),V8的内存限制(64系统下为1.4GB,32系统下为0.7GB),注意的是Buffer存储在堆外存中,会不受v8内存分配的限制。在启动app时可以通过传递参数控制内存的大小:

node --max-old-space-size=1700 test.js  // 单位为MB,设置老生代存储空间的大小 
node --max-new-space-size=1024 test.js  // 单位为KB,设置新生代存储空间的大小
  • 内存指标

Node.js 提供了 process.memoryUsage/os.totalmem/os.freemem 函数来查看内存指标

process.memoryUsage()
=> { 
  rss: 22749184,  // 常驻内存集的大小,就是给该进程分配了多少物理空间,包括堆,栈,代码段
  heapTotal: 7708672, //分配的堆内存大小
  heapUsed: 5078856,  //堆已经使用的内存大小,内存泄漏主要查看该指标
  external: 8614     // V8 引擎内部的 C++ 对象占用的内存
}

//返回操作系统的总内存大小
os.totalmem()
=> 17179869184

//返回操作系统的闲置内存大小
os.freemem()
=> 5585453056
V8的垃圾回收机制
v8垃圾回收算法:

V8 内存分为新生代区和老生代区,一般刚刚新建的对象都存放在新生代区,然后存活周期比较长的对象就会被放入老生代区,新生代区和老生代区都有各自的垃圾回收算法:

  • Scavenge算法(新生代区):

采用复制的方式,将堆内存一分为二,通过将存活的对象在两个块内存之间复制实现,复制后重新整理内存,删除碎片。特点是由于生命周期短的对象只占少部分,时间效率比较高,但是由于只能使用堆内存一半的空间,空间利用率不高,适用于新生代对象;

  • 对象晋升:

在一定的条件下需要将新生代中存活周期长的对象复制到老生代中,完成对象晋升,对象晋升的条件有两个:

- 对象是否经过多次Scavenge回收
- 新生代空间的内存占用比超过限制
  • Mark-Sweep和Mark-Compact算法(老生代区):

老生代重要采用Mark-Sweep和Mark-Compact相结合进行垃圾回收:

- Mark-Sweep 先标记死的对象,然后清除死的对象,会产生碎片。
- Mark-Compact 会在标记死亡对象后,对内存进行整理,无内存碎片产生,速度相对比较慢

V8 的老生代区主要使用 Mark-Sweep,在空间不足的情况下才使用 Mark-Compact 算法。并且使用增量标记回收(Incremental Marking):由于老生代一次完全的垃圾回收耗时长,如果完全停顿下来进行垃圾回收,回收完以后再执行应用逻辑,性能会很差,所以采用 Incremental Marking 方式进行回收,采用增量标记,每做完一个步进,执行一会应用逻辑,提高性能;

查看垃圾回收日志:
  • –trace_gc参数:启动时添加该参数,可以查看 node 日志回收信息:
node --trace_gc app.js
//285 ms: Scavenge 6.8 (41.5) -> 5.6 (42.5) MB, 3.2 ms [allocation failure]

- 程序运行到285毫秒的时候(日志时间,V8采用了相对时间而不是绝对时间)V8对新生代进行了基于Scavenge算法的垃圾回收;
- 新生代内存占用从6.8MB空间释放到了5.6MB(默认最大值是64MB),全局内存占用从41.5MB上升到了42.5MB(默认最大值是1400MB+64MB),用了3.2毫秒。
- 最后的allocation failure目测不是真的失败,不是很清楚具体含义
  • –prof参数:启动时添加该参数,可以生产一个 v8.log 的日志文件,该文件基本不可读,可以用 tick 工具实现对该文件进行分析:
sudo npm install -g tick
node-tick-processor *-v8.log //node-tick-processor 工具解析日志文件

//生成日志分析部分内容如下:
 [C++]:
   ticks  total  nonlib   name

 [GC]:
   ticks  total  nonlib   name
     56    2.0%

 [Bottom up (heavy) profile]:
  Note: percentage shows a share of a particular caller in the total
  amount of its parent calls.
  Callers occupying less than 2.0% are not shown.
  1. 变量的回收:在正常的JavaScript执行中,无法立即回收的内存有闭包和全局变量这两种情况。
内存泄露及解决方案
内存泄漏分类
  • 缓存:不及时释放缓存对象;
  • 模块加载:Node 的模块加载机制是通过缓存的,模块内部的局部变量会被保存,形成闭包,造成内存泄露
//闭包问题
(function (exports, require, module, __filename, __dirname) { 
    var local = "局部变量";
    exports.get = function () { 
        return local;
    }; 
});
  1. 全局对象
global.a = 10;
process.b = 20;
  1. 队列问题:当消费速度低于生产速度时,相关作用域来不及释放,从而出现内存泄露,比如异步批量读取文件,异步批量数据库操作等问题,可以参考朴灵大大写的Bagpipe库,可以限制队列的长度,提供超时模式和拒绝模式,防止内存泄露
var bagpipe = new Bagpipe(10);

var files = ['这里有很多很多文件'];
for (var i = 0; i < files.length; i++) {
  // fs.readFile(files[i], 'utf-8', function (err, data) {
  bagpipe.push(fs.readFile, files[i], 'utf-8', function (err, data) {
    // 不会因为文件描述符过多出错
    // 妥妥的
  });
}
  1. 事件监听导致内存泄漏:

Node.js 的事件监听也可能出现的内存泄漏。例如对同一个事件重复监听,忘记移除(removeListener),将造成内存泄漏。这种情况很容易在复用对象上添加事件时出现,所以事件重复监听可能收到如下警告:

(node:2752) Warning: Possible EventEmitter memory leak detected。11 haha listeners added。Use emitter。setMaxListeners() to increase limit

例如,Node.js 中 Agent 的 keepAlive 为 true 时,可能造成的内存泄漏。当 Agent keepAlive 为 true 的时候,将会复用之前使用过的 socket,如果在 socket 上添加事件监听,忘记清除的话,因为 socket 的复用,将导致事件重复监听从而产生内存泄漏。

我们可以通过设置 setMaxListeners() 来限制最大监听个数:

emitter.setMaxListeners(10);
内存泄露排查工具
  1. node-heapdump 模块:
var heapdump = require('heapdump'); // 在代码中引入

$kill -USR2 <pid>  //启动进程后,我们可以向服务进程发送SIGUSR2信号,生成内存的一份快照

//生成快照的文件格式:heapdump-148010712.625910.heapsnapshot
//将以上文件加载到 chrome 浏览器中开发者工具的 Profiles 中,可以查看内存泄露信息及其它性能问题
  1. node-memwatch:
var memwatch = require('memwatch');
memwatch.on('leak', function(info) {  //当内存的使用量的增加达到连续5次垃圾回收时值时触发 "leak" 事件
    console.log('info:', info);
    /*
    { 
        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' 
    }
   */
});
  1. 使用 --prof 参数
node --prof app.js
    
运行后会生成一个isolate-0xnnnnnnnnnnnn-v8.log文件在当前目录下

你可以使用 --prof-process 来生成报告查看
   
node --prof-process isolate-0xnnnnnnnnnnnn-v8.log
  1. 使用 --perf_basic_prof 参数
node --perf_basic_prof app.js
运行后会生成一个.map文件
可以使用linux自带的perf命令进行分析
  1. v8-profile

我们知道 Node.js 是基于 V8 引擎的,V8 暴露了一些 profiler API,我们可以通过 v8-profiler 收集一些运行时数据(例如:CPU 和内存),并保存在一个大的json文件里,如下:

profiler.startProfiling('1', true);
await sleep(20000);
let profile1 = profiler.stopProfiling();
profile1.export(function(error, result) {
fs.writeFileSync('profile1.json', result);
profile1.delete();

拿到 json 文件后,打开 Chrome -> 调出开发者工具(DevTools) -> 单击右上角三个点的按钮 -> More tools -> JavaScript Profiler -> Load,加载刚才生成的 cpuprofile 文件,便可以进行性能分析。

  1. v8-analytics

对 v8-profile 和 heapdump 产生的日志进行分析,它可以区分 v8 优化失败的函数,根据函数执行时长进行标红区别,展示可疑的内存泄漏点:

'use strict';
const fs = require('fs');
const v8Analytics = require('v8-analytics');
//or you can use following, they're equival
//const v8Analytics = require('v8-cpu-analysis');

//list all js function and it's execTime
const json = JSON.parse(fs.readFileSync('./test.cpu.json'));
const str = v8Analytics(json);
console.log(str);

//list you heap memory info
const json = JSON.parse(fs.readFileSync('./test.mem.json'));
const {leakPoint, heapMap, statistics} = analysisLib.memAnalytics(allData)
参考文献
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值