内存控制是在海量请求和长时间运行的前提下进行探讨的。
在 Node 中如何高效地使用内存?
V8 的垃圾回收机制与内存限制
V8 的内存限制
- Node 通过 JavaScript 使用内存的限制:64 位系统下约为 1.4 GB,32 位系统下约为 0.7 GB。
- Node 基于 V8 构建,所以在 Node 中使用的 JavaScript 对象基本上都是通过 V8 自己的方式来进行管理和分配的。
V8 的对象分配
- 在 V8 中,所有的 JavaScript 对象都是通过 == 堆 == 来分配的。
- V8 限制堆内存,是基于垃圾回收机制与应用性能的考量(垃圾回收会引起 JS 线程暂停执行)。
查看进程的内存占用:process.memoryUsage ()
$ node
> process.memoryUsage();
{ rss: 23175168, # 进程的常驻内存部分
heapTotal: 9682944, # 已申请到的堆内存
heapUsed: 5283000, # 堆内存当前使用量
external: 11777 }
>
# 堆中的内存总量<进程的常驻内存用量
# 堆外内存:不是通过V8分配的内存。
# Buffer 对象不经过 V8 的内存分配机制。
# Node 的内存构成:通过 V8 分配的部分 + Node 自行分配的部分。
# 因此,受到 V8 的垃圾回收机制限制的主要是 V8 的堆内存。
查看系统的内存占用:os.totalmem () 和 freemem ()
查看操作系统的内存使用情况:
$ node
> os.totalmem() # 返回系统总内存
17179869184
> os.freemem() # 返回系统闲置内存
4516331520
Node 启动时可以调整内存使用量:
node --max-old-space-size=1700 test.js // 单位为MB,设置老生代内存空间最大值
node --max-new-space-size=1024 test.js // 单位为KB,设置新生代内存空间大小
V8 的垃圾回收机制
V8 的垃圾回收策略主要基于 == 分代式垃圾回收机制 ==。
V8 的内存分代:
- V8 堆内存:新生代内存 + 老生代内存。
- 新生代的内存空间中的对象:存活时间短。
- 老生代的内存空间中的对象:存活时间长、常驻内存对象。
V8 引擎下的堆内存分配 64 位系统 32 位系统 默认老生代内存最大值 1400 MB 700 MB 默认新生代内存最大值 32 MB 16 MB V8 堆内存的最大值 1464 MB (1.4 GB) 732 MB(0.7 GB) 新生代中的对象主要通过 Scavenge 算法进行垃圾回收,Scavenge 算法主要采用 Cheney 算法(采用复制的方式实现垃圾回收 From-To)实现。
老生代中的对象主要采用了 Mark-Sweep(标记清除)和 Marl-Compact(标记整理)相结合的方式进行垃圾回收。
查看垃圾回收日志:--trach_gc
# 在 gc.log 文件中得到所有的垃圾回收信息。
node --trach_gc -e "var a = [];for (var i = 0; i < 1000000; i++) a.push(new Array(100));" > gc.log
查看性能分析数据:--prof
node --prof test.js
V8 提供了 linux-tick-processor 工具(Node 路径:deps/v8/tools)用于统计日志信息。
# 将该工具的目录添加到环境变量 PATH 中,执行分析日志命令:
linux-tick-processor v8.log
高效使用内存
如何让垃圾回收机制更高效地工作?
作用域
JavaScript 中能形成作用域的方式:函数调用、with、全局作用域。
标识符查找,变量只能向外访问,而不能向内访问;
作用域链;
变量的主动释放:delete 操作、将变量重新赋值。
<!–hexoPostRenderEscape:
// 全局变量,进程退出才会释放
global.foo = 'I am global object';
console.log(global.foo); // => 'I am global object'
// 1. delete 操作删除全局变量
delete global.foo;
// 2. 重新赋值
:hexoPostRenderEscape–>
global.foo = undefined; // or null
console.log(global.foo); // => ‘undefined’
💡 在 V8 中通过 delete 删除对象的属性有可能干扰 V8 的优化,所以通过赋值方式解除引用更好。
闭包:实现外部作用域访问内部作用域中变量的方法
var foo = function () {
var bar = function () {
var local = '局部变量';
return function () { // 函数作为返回值,且该匿名函数可以访问local
return local; // 内部作用域可以访问外部对象
};
};
var baz = bar(); // bar() 函数返回的是一个匿名函数,且可以访问local
console.log(baz());
}
在正常的 JavaScript 执行中,无法立即回收的内存:
- 闭包;
- 全局变量;
慎将内存当缓存
- 在 Node 中,任何试图拿内存当缓存的行为都应当被限制。
- Iru cache
缓存的解决方案
进程之间无法共享内存,使用大量缓存的解决方案:采用进程外的缓存,进程自身不存储状态。
解决方案:
- redis
- Memcached
关注队列状态
消费速度 < 生产速度时,将会形成堆积:
日志收集时,选择更高效的文件写入日志,而不是数据库写入日志。
深度的解决方案:1. 监控队列的长度;2. 任意异步调用都应该包含超时机制。
Bagpipe 的超时模式和拒绝模式。
内存泄漏排查
Node 内存泄漏检测工具:
v8-profiler。由 Danny Coastes 提供,它可以用于对 V8 堆内存抓取快照和对 CPU 进行分析,该项目已有 3 年没维护了- node-heapdump。这是 Node 核心贡献者之一 Ben Noordhuis 编写的模块,它允许对 V8 堆内存抓取快照,用于事后分析。
- node-mtrace。由 Jimb Esser 提供,它使用了 GCC 的 mtrace 工具来分析堆的使用。
- dtace。在 Joyent 的 SmartOS 系统上,有完善的 dtrace 工具用来分析内存泄漏。
- node-memwatch。来自 Mozilla 的 Lloyd Hilaiel 贡献的模块,采用 WTFPL 许可发布。
大内存应用
Node 提供了 stream 模块用于处理大文件。
fs 模块的 createReadStream()
和 createWriteStream()
方法可以分别用于创建文件的可读流与可写流。
process 模块中的 stdin 和 stdout 分别是可读流和可写流的示例。
const fs = require('fs');
// 读取一个文件,然后将数据写入到另一个文件中
// 流处理的方式不会受到V8内存限制的影响。
var reader = fs.createReadStream('in.txt');
var writer = fs.createWriteStream('out.txt');
reader.on('data', function (chunk) {
writer.write(chunk);
});
reader.on('end', function () {
writer.end();
});
// 上述代码的简写方法:
var reader = fs.createReadStream('in.txt');
var writer = fs.createWriteStream('out.txt');
reader.pipe(writer);
💡
如果不需要进行字符串层面的操作,则不需要借助 V8 来处理,可以尝试进行纯粹的 ==Buffer== 操作,这不会受到 V8 堆内存的限制。