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.
- 变量的回收:在正常的JavaScript执行中,无法立即回收的内存有闭包和全局变量这两种情况。
内存泄露及解决方案
内存泄漏分类
- 缓存:不及时释放缓存对象;
- 模块加载:Node 的模块加载机制是通过缓存的,模块内部的局部变量会被保存,形成闭包,造成内存泄露
//闭包问题
(function (exports, require, module, __filename, __dirname) {
var local = "局部变量";
exports.get = function () {
return local;
};
});
- 全局对象
global.a = 10;
process.b = 20;
- 队列问题:当消费速度低于生产速度时,相关作用域来不及释放,从而出现内存泄露,比如异步批量读取文件,异步批量数据库操作等问题,可以参考朴灵大大写的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) {
// 不会因为文件描述符过多出错
// 妥妥的
});
}
- 事件监听导致内存泄漏:
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);
内存泄露排查工具
- node-heapdump 模块:
var heapdump = require('heapdump'); // 在代码中引入
$kill -USR2 <pid> //启动进程后,我们可以向服务进程发送SIGUSR2信号,生成内存的一份快照
//生成快照的文件格式:heapdump-148010712.625910.heapsnapshot
//将以上文件加载到 chrome 浏览器中开发者工具的 Profiles 中,可以查看内存泄露信息及其它性能问题
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'
}
*/
});
- 使用 --prof 参数
node --prof app.js
运行后会生成一个isolate-0xnnnnnnnnnnnn-v8.log文件在当前目录下
你可以使用 --prof-process 来生成报告查看
node --prof-process isolate-0xnnnnnnnnnnnn-v8.log
- 使用 --perf_basic_prof 参数
node --perf_basic_prof app.js
运行后会生成一个.map文件
可以使用linux自带的perf命令进行分析
我们知道 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 文件,便可以进行性能分析。
- 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)