node的内存限制:

  1. 内存限制和原因:

对于node来说,内存一直是限制node在后端广泛深度使用的原因,因为node的v8引擎能从服务器的内核中分配出来的内存不多:

对于32位的系统,v8分配出来的内存一般只有0.7g;

对于64位系统,v8分配出来的内存一般只有1.4g;

因此如果将一个比较大的内容存储在内容或者一个存储在内存的体积超过了对应系统v8的上限,就会出现out of memory,从而退出node进程(单进程)。

表面原因:是因为v8不但要使用在服务端环境,还需要在浏览器环境下使用,所以为了权衡两者对内村的需要,只能得到上述的v8内存限制;

内在原因:在v8引擎中,GC进行一次,此时js线程就会暂停,等待GC回收完成再继续未完成的工作,而分配给v8的内存越大,GC的耗时可能就会越大,这样会降低响应的效率,例如给分配1.5g内存,GC回收一个大的对象时可能会需要1s钟,因此才会对v8的内存做出了限制。

  1. 查看内存使用率:

可以通过node提供的API来查看node的内存:

const {rss, heapTotal, heapUsed, external} = process.memoryUsage()
{
    rss:node常驻内存的大小,包括v8内存,堆外内存和c++等内存的总和;
    heapTotal:v8申请的总内存;
    heapUsage:v8使用的总内存;
    external:c++里面和js产生关联的对象内存;
}
  1. 查看GC回收耗时

3.1 : node --trace_gc index.js:通过--trace_gc来查看gc日志信息;

image.png
(输出的信息可读性不强)

3.2 node --prof index.js
该命令可以执行完js文件之后,生成一系列的统计数据,输出一个xxxx-v8.log的日志文件;有两种途径分析这个文件:

1)下载tick的npm包:

sudo npm install tick -g

该npm包可以分析v8.log文件,然后输出一些列的统计数据;

node-tick-processor xxx-v8.log

image.png
图中通过207个tick(系统时钟)总占比15.1%;

2)node的v5版本后可以通过 --prof-processor处理该文件

node --prof-process xxxx-v8.log

该指令可以详细的输出内存占比和耗时。

image.png

垃圾回收方式:

  1. 一般引用计数:

原理解释:就是一个值被赋予给其他变量,那么该值的引用次数+1;如果含有该值的变量更新了其值,那么该值的引用次数就减一,直到整个执行周期结束后,该值的引用次数为0,那么此时就会被垃圾回收;

let a = {a1: 1};
let b = a // 此时a的计次为1
b = 0 // 此时a的计次为0;

存在问题:就是存在相互引用的现象出现,从而引发内存泄露;

// a.js
import b from 'b.js'
export let a = {c: b}

// b.js
import a from 'a.js'
export let b = {c: a}
  1. node的GC方式:

node的v8引擎将出现在内存中的变量分为新生代和老生代,新生代是表示存活时间不长的变量,老生代就是存活时间较长或者常驻内存的变量,所以v8申请的内存会划分为两部分分给这两种生代的变量使用,对应的回收这两种生代的GC也不一样;

image.png

对于这两种生代的内存分配,v8也是有相关的限制:
新生代的内存上限:64位 ----> 64MB内存占用; 32位 ---> 32MB内存占用;
老生代的内存上限:64位 ----> 1400MB内存占用; 32位 ---> 700MB内存占用;

对于老生代,还可以细分出几块区域:

a) old object space 即大家口中的老生代,不是全部老生代,这里的对象大部分是由新生代晋升而来
b) large object space 大对象存储区域,其他区域无法存储下的对象会被放在这里,基本是超过 1M 的对象,这种对象不会在新生代对象中分配,直接存放到这里,当然了,这么大的数据,复制成本很高,基本就是在这里等待命运的降临不可能接受仅仅是知其然,而不知其所以然
c) Map space 这个玩意,就是存储对象的映射关系的,其实就是隐藏类;
d) code space 简单点说,就是存放代码的地方,编译之后的代码,是根据大佬们写的代码编译出来的代码
2.1 scavenge算法:
对于新生代,v8会采用空间换时间的方式,将新生代的空间划分成一半是From空间,一半是To空间;From空间主要是用来分配对象,并将程序在运行中新生的对象所占的内存储存在该空间,而To空间则是在开启垃圾回收时,对于From空间中还存活的变量会移植到该空间;From空间和To空间不会时一成不变,他们是动态切换,也就是说当进行玩一次垃圾回收后会将这两个空间进行对换;当然如果一个变量连续两次都存活在To空间,就会认为该变量需要存活很久,所以需要将其移植到另一个空间存储,这称为晋升。

此外在新生代中这两个空间都简称为semiSpace空间。

以下就是scavenge算法的一次垃圾回收的过程:
首先:在程序运行阶段会将分配给新创建对象的内存空间,是从From空间分配;
image.png

其次当开启垃圾回收的时候,每个From空间中的变量都会进行判断,判断是否可以进入To空间,还是移植到另一个空间,判断的依据是:
1) 该变量已经在上一次的垃圾回收中存在过To空间;
2) To空间已经占据了整个空间的25%;
此时该变量都会进入到老生代的空间;
image.png

最后,等所有变量都复制到To空间后,此时就会交换空间,To空间会变成From空间,From空间会变成To空间,因此原来To空间中的变量就会继续在From空间中存活;
image.png

缺点:空间换时间,减少了存储变量的内存,因此只能用来进行小规模的垃圾回收;
优点:回收的效率高,回收一次的耗时很小,不太影响正常的响应;

注意:25%是因为新生代只占据着这个内存的50%,而from和to按理应该各分到25%,如果其中一方分到超过25%,那么在交换空间的时候,另一方就会出现不平衡状态,此时就会将数据转移到老生代。

2.2 mark-sweep:
在老生代中,会采用标记清除的方法进行垃圾回收,对于该空间的变量一开始会标记为"存活"的标记,然后当进行垃圾回收后会检查该空间是否没有标记的变量,如果没有则清理;

2.3 mark-compact:
在老生代中处理mark-sweep外还会存在另一种回收方式,因为前者在删除后会出现很个不连续的"坑",以至于出现很多碎片,影响内存的分配,所以需要在清除之前,将标记的变量移到一边,剩余没标记的变量集合在一边同意清除,减少碎片内存;

在速度来说m-s比m-c要快,但碎片mc比m-s要少;

2.4 优化垃圾回收:

全停顿:当开启一次垃圾回收时,当前执行的代码会被停止,等待垃圾回收完成后再继续后续的执行,对于新生代来说由于新生的变量存活的时间不长,因此全停顿的时间不长,影响不大,但是对于老生代来说,因为m-s和m-c都是比较耗时,所以如果完全等待其回收完,全停顿的时间会很长,因此会影响到正常的业务运作;

增量标记:正如上述所说,对于老生代的垃圾回收所带来的全停顿代价较高,需要优化,而老生代的回收是分为几个阶段,标记-清除-整理,而一般最耗时的是处于标记,所以采用了将标记进行拆分成多个小分片,每执行完一个小分片,就会恢复一段js逻辑让其执行一会儿,然后再继续执行分片的回收,直到所有的分片回收完成,完后再执行执行后续的js逻辑。

延迟清除:这个也很简单,在标记之后,引擎清楚直到哪些是可以清除的对象,但是并不代表需要同时清除掉这些垃圾,所以引擎选择按需清理,优先从需要的页面开始,逐步清理所有的页面垃圾,然后就算就完成了一整个垃圾回收周期。

node引发内存泄露:

  1. 闭包:

闭包本身不会引发内存泄露,因为放在闭包的中变量是需要存活在老生代的变量,是使用其所需要的变量,而不是无用的变量;而且也不会因为一两次使用闭包就会出现内存泄露;

但是闭包如果使用不当,也会引发内存泄露,如使用的位置在全局,因此有如下几个:

1) 请勿在闭包中进行循环引用,这样会造成比较严重的内存泄漏。

2) 关于函数中调用的定时器,在不使用时,需要及时清除掉。

3) 尽量不要使用全局变量定义闭包的引用,因为全局变量仅会在页面刷新时被回收【除非手动清除】;

4) 为了避免闭包的内存泄漏,最好在函数引用的变量不被使用时,给其赋值为null[指向空],这样内存将会被回收;

  1. 缓存:

缓存一向是后端优化性能和响应速度的首选,然而对于node来说,如果采用缓存,一不小心就会出现内存泄露从而引发oom现象;因为后端的应用是一个单例再运行,不会让其停止,所以如果长年累月的缓存数据,最后会有可能超出node的v8内存上限;

const cache = {};
function get(key) {
    if (cache[key]) {
        return cache[key];
    } else {
        cache[key] = IO操作;
        return cache[key];
    }
}

正如上面所示,要再node中使用缓存必须要对其进行约束:
1)加上过期清理机制;
2)制定阀值,超过阀值删除可以删除的内容;

  1. 消费队列消费速度小于生产速度:

当消费的速度小于生产的速度,就会出现队列过长,以至于超过上限,因此需要对这种情况,队列的长度做出限制;

  1. 定时器和事件回调函数用完后不清理;
  2. dom节点的引用用完没有被设为null;

node解决内存泄露的工具:

  1. node-heapdump:

用来捕捉内存的快照,会生成一个heapdump文件,该文件是一个json文件,需要利用chrome的profile引入该文件才能查看:
使用方式:

npm install heapdump

const heap = require('heapdump');
heapdump.writeSnapshot('xxxx.heapsnapshot')

或者:
该工具使用简洁,不仅提供了生成快照的方法,还可以在命令行中执行,只要引入该npm包就行;
kill -USR2 <pid>
来发送信号生成堆转储文件。

该文件需要利用到chrome查看:
image.png
介绍1:
Summary:以构造函数名分类显示
Comparison:比较多个快照之间的差异
Containment:查看整个 GC 路径
Statistics:以饼状图显示内存占用信息

image.png
介绍2:
Contructor:构造函数名,如 Object、Module、Socket,(array)、(string)、(regexp) 等加了括号的分别代表内置的 Array、String、Regexp。
Distance:到 GC roots (GC 根对象)的距离。GC 根对象在浏览器中一般是 window 对象,在 Node.js 中是 global 对象。距离越大,说明引用越深,则有必要重点关注一下,极大可能是内存泄漏的对象。
Objects count:对象个数,即展开有多少项。
Shallow Size:对象自身大小,不包括它引用的对象。
Retained Size:对象自身大小和它引用对象的大小,即该对象被 GC 之后所能回收的内存的大小。

  1. memwatch

该npm包可以观察gc的回收情况,通过提供的事件监听从而得出它经历过几次GC,包括scavenge,mark-sweep,increase mark等;
使用时采用爱彼领提供的node-memwatch的npm包:
npm i @airbnb/node-memwatch

常用的api:
stats事件:如果进行一次gc,就会触发该事件,进行多少次gc回收,就会触发多少次stats事件;

memwatch.on('stats', function(stats) { ... });

image.png
(里面的时间是采用ns,包含着scavenge,mark-sweep-compace,increate-mark)

HeapDiff: 通过包裹一个执行体,然后会得到两个快照,并对比两个快照,得出他们的差异点;

const memwatch = require('@airbnb/node-memwatch');
const hp = new memwatch.HeapDiff()
var a = [];
for (let i = 0; i < 1000000; i++) {
    a.push(new Array(100));
}
const diff =  hp.end()
console.log('diff',diff)

image.png


DragonChen
285 声望15 粉丝

下一篇是:Axios源码解析。