依托 V8 的 NODE 在使用内存时是有大小限制的,具体大小与系统类型、版本、NODE 版本相关(64 位系统 1.4GB 和 32 位系统 0.7 GB 的大小)。
NODE 与 C++不同,垃圾回收 GC 由系统自动执行,不由开发者参与,当代码使用不当时,会导致 GC 不能正确回收发生内存泄漏。
V8 对象(内存)分配 及回收
> node --max-old-space-size=1700 test.js // 单位为MB,老生代内存大小限制
> node --max-new-space-size=1024 test.js // 单位为KB,新生代内存大小限制
>
> process.memoryUsage() // 查看内存使用情况
< {
rss: 14958592,
heapTotal: 7195904, // 申请到的堆内存
heapUsed: 2821496 // 使用的量
}
-
V8 内存分代:新生代中的对象存活时间较短;老生代中的对象长驻或常驻内存;
- 老生代在 64 位系统下默认大小限制为 1400 MB,在 32 位 700 MB
- 老生代在 32 位系统下默认大小限制为 32 MB,在 32 位 16 MB
-
回收算法:Scavenge 算法(新生代)、Mark-Sweep & Mark-Compact(老生代两者结合使用)
- Scavenge:将堆内存一分为二,二者只有一个处理使用状态,称为 From 空间,空闲的为 To 空间。
- 分配对象在 From 空间。
- 垃圾回收时,释放 From 中非活对象,复制存活对象到 To 空间。
- 完成复制后两个空间的角色发生对换(又称翻转)。
- 晋升:当一个对象多次复制后依然存活,则移动到老生代中;或 To 空间使用超过 25%。
- Mark-Sweep:标记活着的对象,回收没有标记的对象。(会造成空间不连续)
- Mark-Compact:将活着的对象往一端移动,移动完成后,清理掉边界外的内存。
- Scavenge:将堆内存一分为二,二者只有一个处理使用状态,称为 From 空间,空闲的为 To 空间。
-
回收执行时机
- 全停顿 stop-the-world:GC 执行时需要将应用暂停,完成 GC 再恢复(时间代价较大)
- 增量标记 incremental marking:(老生代)从标记阶段入手,拆分为小步,逻辑执行与 GC 交替执行
- 后续还引入了延迟清理(lazy sweeping)与增量式整理(incremental compaction),让清理与整理动作也变成增量式的,进一步利用多核性能。
-
GC 耗时分析:Node 启动时使用--prof 参数,可以得到 V8 执行时的性能分析数据。但得到的日志文件不具备可读性,需要借助工具(linux-tick-processor、windows-tick-processor.bat)
linux-tick-processor v8.log
来分析 GC 的耗时
作用域和闭包对内存的影响
-
形成作用域:函数调用、with、全局作用域
- 局部变量分配在作用域空间上,随作用域回收而释放。如果变量小周期短会被分配到新生代的 Form 空间。
- 作用域链:在当前作用域中无法找到变量的声明,将会向上级的作用域里查找,直到查到为止。
- 变量的主动释放:=undefined 或 delete 对象。在 node10 中,对象和计算量多的情况下,执行 =undefined 比 delete 大部分时间是快的,在 chrome90 中更明显。
-
闭包:外部作用域访问内部作用域中变量的方法叫做闭包(closure)
- 比如 A 方法返回 B 函数,B 访问 A 中变量,a = A(),只要 a 不被释放 B 占用的内存就得不到释放。(a 不使用了及时=undefined,以释放 B)
查看内存使用情况
> process.memoryUsage() // 查看内存使用情况
< {
rss: 14958592, // 常驻集大小,包括所有 C++ 和 JavaScript 对象和代码;
heapTotal: 7195904, // 申请到的堆内存
heapUsed: 2821496, // 使用的量
external: 13522, // 绑定到 V8 管理的 JavaScript 对象的 C++ 对象的内存使用量。
arrayBuffers: 15159 // 所有 Node.js Buffer
}
> os.totalmem() // 系统的总内存
< 8589934592
> os.freemem() // 系统闲置内存
< 185921536
- rss: resident set size 常驻集大小
- external:外部的,堆外内存
heapTotal 不包含 rss、arrayBuffers、external。external 包含 arrayBuffers。批量操作 buffer 后,heapTotal、heapUsed 不变,arrayBuffers、external 变大(rss 短时涨一些,很快就下去了)
内存泄漏
-
常见原因:意外的全局变量、没有及时清理的计时器或回调、闭包
-
慎将内存当做缓存,比如 store 中的大对象、已经不用的变量,导致内存泄漏甚至溢出。存储需求强烈可以使用 Redis 或 indexdb 等不占用 v8 缓存限制的方式。
-
日志收集时,写入操作慢于日志产生,js 队列数据过大导致内存溢出,或记录日志的 js 相关作用域得不到释放,出现内存泄漏。
内存泄漏排查工具
-
v8-profiler,可以用来分析 cpu(书中未详说)
-
node-heapdump:
npm i heapdump
;在代码的第一行添加如下代码将其引入;kill -USR2 PID
生成分析文件;导入 chrome 中的 Profiles 中进行分析,根据新生代(shallow size)、老生代(retained size)内存占比推测泄漏的数据 -
node-memwatch:
npm i memwatch
;通过改变 heapDiff 的开始位置,或许可以逐步定位泄漏的位置,通过 diff 结果可以推测泄漏的为数组memwatch.on("stats", cb) // 全堆垃圾回收 memwatch.on("leak", cb) // 内存泄漏(如连续5次垃圾回收内存仍未释放) var hd = new memwatch.HeapDiff(); /* 要分析的代码 */ ... var diff = hd.end(); // 内容差异结果 /* { what: "String", size_bytes: 879424, size: "858.81 kb", +: 20001, // 分配的字符串对象数量 -: 1 // 释放的字符串对象数量 } */
-
流(stream)或管道(pipe)专门用来操作需要大内存的数据,且不受 V8 内存限制的影响
-
纯粹的 Buffer 操作(不涉及字符串),也不受 V8 内存限制的影响
网友评论