nodejs内存控制

  • v8的内存限制
  • v8的垃圾回收机制
  • 高效使用内存与内存指标
  • 内存泄漏与内存泄漏排查
  • 大内存应用

 一、v8的内存限制

1.1为什么要关注内存?

在JavaScript中,它与Java一样都是由垃圾回收机制来进行自动内存管理,这使得开发者不需要像C/C++开发那样时刻关注内存的分配和释放问题。所以在开发浏览器的前端页面时,我们基本不关心内存的管理问题,这种不关心不代表问题不存在,一方面时JavaScript内核的内存管理机制基本能应付大部分的内存管理问题,另一方面是在客户端没有像服务上那样对性能机制最求的需求,浏览器内核的垃圾回收机制有足够的时间去实现内存回收操作。但这并不代表绝对的安全,在操作不慎的情况依然会有出现内存溢出的可能,关注内存管理或许不是在前端页面开发中的首要任务,但也不是可以忽略的问题。

这一节的内容显然不是围绕前端开发展开的,在nodejs项目开发中对于内存管理的要求是要明显区别于浏览器端的页面开发。在使用nodejs开发对性能敏感的服务器端程序时,内存管理的好坏、垃圾回收状况是否优良,都会对服务够成影响。

v8以事件驱动、非阻塞I/O和其优秀的性能表现被Ryan Dahl选择作为nodejs的JavaScript脚本引擎,但同样也受到v8的限制,特别是这里我们需要讨论的内存管理问题。

1.2v8的内存限制

nodejs中通过JavaScript使用内存时只能使用部分内存,(64位系统下约为1.4GB;32位系统下约为0.7GB)这导致即使物理内存可能有32GB,你也无法将2GB的文件读如内存。造成这一问题的原因就是使用JavaScript对象基本上都是通过v8自己的方式分配和管理的,这在浏览器应用场景下绰绰有余,但在nodejs中这却限制了开发者随心所欲的使用内存的想法。即使大部分情况下服务端也不会经常有使用大内存的场景,但这就如同带着镣铐跳舞,如果实际中不小心碰触到这个界限,会造成进程退出

要了解v8内存的使用量及为何限制内存,就需要回到v8在内存使用策略上,知道其原理才能避免问题并更好的管理内存。

1.3v8的对象分配

在v8中,所有JavaScript对象都是通过堆来进行分配的,nodejs中提供了v8中内存使用量的查看方式:

console.log( process.memoryUsage());
{
    rss: 18907136,       //常驻内存
    heapTotal: 4014080,  //当前v8申请使用的总的(堆)内存大小
    heapUsed: 2286912,   //当前脚本实际使用的内存大小
    external: 801290,    //扩展的内存大小
    arrayBuffers: 9382   //独立的空间大小(不占用v8申请使用的内存大小)
}

v8的堆示意图:

 

当代码中声明变量并赋值时,所使用对象的内存就分配到堆中。如果已申请的堆空闲内存不够分配新的对象,将继续申请堆内存,直到堆的大小超过v8的限制为止。

比如上面的测试代码中打印的已申请的内存(heapTotal)为:4014080 / 1024 / 1024 = 3.83MB,随着heapUsed的实际使用值逐渐增长至3.83时v8就会继续申请内存,直到申请到1.4GB(假设系统为64位)并被消费完时就会导致进程退出。

从表面上看限制堆的大小是因为v8最初为浏览器的页面设计的JavaScript引擎,对于网页来说v8限制的堆大小绰绰有余,而实际上却是v8的垃圾回收机制的限制。按照官方的说法以1.5GB堆内存垃圾回收为例,v8做一次小的垃圾回收需要50毫秒以上,做一次非增量式的垃圾回收需要1秒以上。

在垃圾回收时会导致JavaScript线程暂停执行,这种由垃圾回收导致的性能直线式下降对于浏览器页面来说都是一个不能接受的性能损耗,更别说nodejs作为后端服务,所以直接限制堆内存是最好的选择。当然这种限制也不是不能打开,v8提供了两个解除限制的启动标志:--max-old-space-size 、--max-new-space-size来调整内存限制的大小:

node --max-old-space-size=1700 test.js // 单位MB(老生代堆内存最大空间)
node --max-new-space-size=1024 test.js //单位KB(新生代堆内存最大空间)

注意这种手动设置堆内存限制大小只在v8初识化时生效,一旦生效就不能再动态改变。关于老生代和新生代内存在2.1中会有介绍。

 二、v8的垃圾回收机制

 2.1v8的垃圾回收策略主要基于分代式垃圾回收机制:

在实际的应用中,对象的生命周期长短不一,不同的垃圾回收算法只能针对特定情况具有最后好的效果。为此,统计学在垃圾回收算法的发展中产生了很大的作用,现代的垃圾回收算法中按对象的存活时间,将内存的垃圾回收进行不同的分代,然后分别对不同的内存实施更高效的算法。

在v8中,主要将内存分为新生代和老生代两代。新生代的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。

查看v8的源码会发现关于新生代内存空间实际上为两个独立的新生代内存够成,单个新生代内存设定在64位系统和32位系统上分别式16MB和8MB,所以新生代内存的最大值实际上是64位系统和32位系统分别是32MB和16MB。

同样源码中也可以看到老生代内存最大申请空间位64位系统和32位系统分别为1400MB和700MB,但v8的内存空间最大申请限制并不是直接将老新生代的最大限值加起来,而是(4*单个新生代内存限值+老生代内存限值),所以v8堆内存的最大限值在64位系统和32位系统上分别是(16*4+1400=1464MB)和(8*4+700=732MB)。

这也就是前面讲到的64位系统上只能使用1.4GB内存和32位系统上只能使用0.7G内存的由来。

v8垃圾回收算法:Scavenge、Mark-Sweep、Mark-Compact、Incremental Marking

前面解析了内存限制和堆内存分代管理机制,接下来就通过垃圾回收算法来深度理解v8的垃圾回收机制。

Scavenge算法:

新生代的对象主要通过Scavenge算法进行垃圾回收处理,在Scavenge的具体实现中,主要采用了Cheney算法,该算法由C.J.Cheney于1970年首次发布在ACM论文上。

Cheney是一种采用复制的方式实现垃圾回收算法,它将内存一分为二,每一部分空间称为semispace。在这两个semispace空间中,只有一个处于使用中,另一个处于闲置状态。处于使用状态的semispace空间称为From空间,处于闲置状态的空间称为To空间。

当分配对象时首先在From空间上进行分配,当开始进行垃圾回收时,会先检查From空间中的存活对象,这些存活的对象将被复制到To空间中,而非存活对象占用的空间会被释放。完成复制后,From空间和To空间的角色发生对换。简单的说就是将存活对象在两个semispace空间之间复制,这是典型的用空间换时间的算法,其缺点就是只能使用实际占用内存的一半空间,这也是前面提到的源码中新生代内存空间实际上为两个独立的内存够成的原因,这两个独立空间加上老生代对象使用的独立内存空间,就是v8堆内存实际使用堆内存的内存大小。

Scavenge除了基于Cheney算法使用复制的方式释放内存,还会根据检查对象的内存地址是否经历过一次Scavenge回收,如果经历过一次会将对象从From空间复制到老生代空间中,如果没有则复制到To空间中。

除了经历过一次Scacenge回收的对象会被晋升以外,当To空间的内存占比超过25%时,当前检查的对象就会直接晋升到老生代内存空间。下面是两种新生代对象晋升的逻辑示意图:

 

最后关于25%的限制值,是因为这个To空间在完成复制后要转换成From空间,接下来还需要继续为新的对象分配内存。

Mark-Sweep & Mark-Compact算法:

老生代空间中的对象因为存活占比较大,采用Scacenge的复制方式回收一旦存活对象较多,复制对象的效率就会降低,内存空间利用上也会浪费非常多。

由于老生代中的对象生命周期都会比较长,死亡对象占比较低,所以v8中使用Mark-Sweep标记清除和Mark-Compact标记整理的方式来实现内存回收。

Mark-Sweep标记清除:Mark-Sweep算法先遍历堆中的所有对象,并标记出存活对象,然后清除没标记的死亡对象。

 

Mark-Compact标记整理:Mark-Compact是由Mark-Sweep演变而来,它们的差比就是在对象标记死亡以后,在整理过程中将活着的对象往一端移动,然后清除死亡对象和已经被移动走的对象空间。

为什么有了Mark-Sweep标记清除还需要Mark-Compact标记整理呢?它们两者又有什么区别呢?这是因为Mark-Sweep标记清除会导致内存碎片化,回对后续内存分配造成问题,比如在内存空间处于碎片化状态时,突然需要分配一个大对象内存,而这些碎片化的内存空间里没有这么大的内存片段就会出问题。为了解决这种碎片化内存的问题就有了Mark-Compact的标记整理,但是标记整理由于需要多做一个移动操作,所以它的速度肯定会比Mark-Sweep要慢。

但是需要注意Mark-Compact做移动操作是一个非常低效的操作,所以Mark-Sweep和Mark-Compact不仅仅是递进关系,还是两者结合使用的关系,Mark-Compact只有在空间不足以对新生代晋升过来的对象分配内存时才会使用。

Incremental Marking算法:

前面三种算法在进行垃圾回收的时候,都需要将应用逻辑暂停,等执行完垃圾回收后再恢复执行应用逻辑,这种行为被称为“全停顿(stop-the-world)”。在新生代中做一次垃圾回收由于默认配置较小,需要消耗的时间不会太长,但老生代中1G多的内存遍历标记整理操作绝对不会是一个短时间能完成。为了避免全堆垃圾回收带来的应用逻辑长时间停顿问题,v8又引进了Incremental Marking增量标记,在前面三种回收算法基础上来通过Incremental Marking增量标记实现“步进”式垃圾回收。

所谓增量标记实现“步进”式垃圾回收,就是将原本一次应用逻辑停顿完成全部标记操作拆分成许多个小“步进”,每做完一个“步进”就让JavaScript应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行。所谓增量标记实现“步进”可以理解为一次完整的标记操作会被拆分成很多步来完成。

 

v8经过增量标记改进垃圾回收机制后,垃圾回收的最大停顿时间可以减少到原本的1/6左右。随这v8的不断发展,后续还引入了延迟清理(lazy sweeping)和增量式整理(incremental compaction),让清理与整理动作也变成了增量式的操作。

2.2查看垃圾回收日志(监测v8垃圾回收性能)

2.2.1基于--trace_gc指令参数生成垃圾回收日志:

//index.js 测试生成垃圾回收日志的代码
let a = [];
for(let i = 0; i < 1000000; i++){
    a.push(new Array(100));
}

测试指令:

node --trace_gc  .\index.js >gc.log 

执行完以后会在当前工作目录下生成一个gc.log的垃圾回收日志文件,日志片段:

[1828:0000016323844600]       47 ms: Scavenge 2.3 (3.0) -> 1.9 (4.0) MB, 0.9 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
[1828:0000016323844600]       89 ms: Scavenge 2.9 (4.5) -> 2.7 (5.5) MB, 1.0 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
...

2.2.2基于--prof指令参数生成v8执行时的性能分析数据日志(还是使用前面的测试代码):

node --prof .\index.js

执行完以后会在当前工作目录下生成一个....v8.log日志文件,这个文件就是v8性能分析数据日志。

2.2.3基于nodejs內置统计日志工具统计日志信息:

Linux系统:linux-tick-processor

Windows系统:windows-tick-processor.bat

这个內置工具在Nodejs源码的deps/v8/tools目录下,将该目录添加到环境变量中就可以直接通过启动这个工具查看v8的运行日志。

 三、高效使用内存及内存指标

 3.1高效使用内存

在v8面前,开发者所要具备的责任是如何让垃圾回收机制更高效。要讨论这个话题就不得不了解JavaScript作用域、作用域链、闭包,这部分内容在之前的JavaScript的博客中有详细的介绍,可以参考一下博客内容:javascript的作用域和闭包(三)闭包与模块,这是一个系列博客的最后一篇,在这篇博客的开头出列出了系列的其他博客连接,所以就不全部粘贴了。接下来就分析作用域、作用域链、闭包与内存之间的关系,以及如何在开发中实现高效的垃圾回收机制。

标识符查找:

JavaScript中的标识符可以理解为变量名,v8会从当前的作用域向全局作用域逐级查找,如果变量是全局变量,由于全局作用域要直到进程退出才能释放,这种情况就会导致变量常驻在老生代内存空间中,通过前面对v8垃圾回收机制的分析知道老生代中的垃圾回收效率是比较低的,释放全局作用域上的变量可以通过delete删除引用关系。

如果变量是在非全局作用域上,想主动释放变量引用的对象可以通过delete和重新赋值,并且两者具有相同的效果。但v8中通过delete删除对象的属性有可能干扰v8的优化,所以通过赋值的方式解除引用更好。

通过重新赋值的方式解除引用实际上就是切断变量对原对象内存的引用,并在内存中找一篇新的内存片段存放数据,原来的对象内存就会变成死对象,会被垃圾回收机制回收。

闭包与垃圾回收:

JavaScript中闭包可以实现在作用域外部引用作用域内部的变量,这得益于高阶函数的特性。但同样带来比较严重的潜在风险,就是被外部引用的变量可能出现不会即时释放的情况,同时由这个没有及时释放的变量导致作用域产生的内存占用也不会及时释放。这种不及时释放的闭包将会给程序带来严重的内存管理风险,所以在JavaScript中有提出函数尾调用(JavaScript函数尾调用与尾递归)的编程范式来规避这种风险,虽然这不是一个完美的方案但还可以在一定程度上避免一些不必要的内存风险。

3.2内存指标:

前面提到过使用process.memoryUsage()可以查看内存使用情况,除此之外OS模块中的totalmem()和freemenm()方法也可以查看内存使用情况。

查看进程的内存占用:

前面提到使用process.memoryUsage()可以查看Node进程的内存占用情况,为了更好的查看内存占用情况,这里先写一个工具方法将字节为单位的数据转换成MB单位:

 1 const showMem = function(){
 2     let mem = process.memoryUsage();
 3     let format = function(bytes){
 4         return (bytes / 1024 / 1024).toFixed(2) + 'MB';
 5     };
 6     console.log('rss:' + format(mem.rss) 
 7               + ' heapTotal: ' + format(mem.heapTotal) 
 8               + ' heapUsed: ' + format(mem.heapUsed) 
 9               + ' external: ' + format(mem.external) 
10               + ' arrayBuffers: ' + format(mem.arrayBuffers));
11     console.log('---------------------------------------------------------------------------------------------------');
12 };

然后在写一个方法不停步的分配内存并不释放内存,再查看打印的内存数据变化:

 1 let useMem = function(){
 2     let size = 20 * 1024 * 1024;
 3     let arr = new Array(size);
 4     for(let i = 0; i < size; i++){
 5         arr[i] = 0;
 6     }
 7     return arr;
 8 };
 9 
10 let total = [];
11 for(let j = 0; j < 15; j++){
12     showMem();
13     total.push(useMem());
14 }
15 showMem();

打印结果如下(部分打印结果,最后会出现内存溢出)

rss:18.02MB heapTotal: 3.83MB heapUsed: 2.16MB external: 0.76MB arrayBuffers: 0.01MB
---------------------------------------------------------------------------------------------------
rss:180.61MB heapTotal: 164.51MB heapUsed: 162.66MB external: 0.94MB arrayBuffers: 0.01MB
---------------------------------------------------------------------------------------------------
rss:341.23MB heapTotal: 325.51MB heapUsed: 322.45MB external: 0.94MB arrayBuffers: 0.01MB
---------------------------------------------------------------------------------------------------
rss:501.57MB heapTotal: 487.77MB heapUsed: 482.48MB external: 0.94MB arrayBuffers: 0.01MB
---------------------------------------------------------------------------------------------------
rss:661.83MB heapTotal: 651.77MB heapUsed: 642.48MB external: 0.94MB arrayBuffers: 0.01MB
---------------------------------------------------------------------------------------------------
rss:822.14MB heapTotal: 819.77MB heapUsed: 802.48MB external: 0.94MB arrayBuffers: 0.01MB
---------------------------------------------------------------------------------------------------
rss:982.73MB heapTotal: 995.78MB heapUsed: 962.45MB external: 0.94MB arrayBuffers: 0.01MB
---------------------------------------------------------------------------------------------------
rss:1142.85MB heapTotal: 1156.03MB heapUsed: 1122.48MB external: 0.94MB arrayBuffers: 0.01MB
......

从测试的结果来看,只有前面的rss、heapTotal、heapUsed三个数据在不断的增长,后面的external和arrayBuffer都几乎不会发生变化。这里就先来分析前面三个数据:

rss是resident set size的缩写,即进程的常驻内存部分,进程的内存总共分为三部分:一部分是srr常驻内存,其余是交换区swap或者文件系统中。

heapTotal和heapUsed对应的是v8的堆内存信息,heapTotal是总共申请的内存量,heapUsed表示目前堆中使用的内存量。

external是指绑定到v8管理的JavaScript对象的C/C++内存使用量,简单的理解就是nodejs的C/C++扩展使用的内存数据。

arrayBuffers通常被称为对外内存或者独立内存,是由ArrayBuffer 和 SharedArrayBuffer 分配的内存。这里改造一下内存测试代码:

 1 let useMem = function(){
 2     let size = 200 * 1024 * 1024;
 3     let buffer = new Buffer(size);
 4     for(let i = 0; i < size; i++){
 5         buffer[i] = 0;
 6     }
 7     return buffer;
 8 };
 9 let total = [];
10 for(let j = 0; j < 15; j++){
11     showMem();
12     total.push(useMem());
13 }
14 showMem();

查看打印测试结果:

rss:18.02MB heapTotal: 3.83MB heapUsed: 2.17MB external: 0.76MB arrayBuffers: 0.01MB
---------------------------------------------------------------------------------------------------
rss:221.02MB heapTotal: 4.50MB heapUsed: 2.85MB external: 200.94MB arrayBuffers: 200.01MB
---------------------------------------------------------------------------------------------------
rss:421.66MB heapTotal: 5.25MB heapUsed: 2.64MB external: 400.94MB arrayBuffers: 400.01MB
---------------------------------------------------------------------------------------------------
rss:622.00MB heapTotal: 7.75MB heapUsed: 2.02MB external: 600.93MB arrayBuffers: 600.01MB
---------------------------------------------------------------------------------------------------
rss:822.01MB heapTotal: 7.75MB heapUsed: 2.02MB external: 800.93MB arrayBuffers: 800.01MB
---------------------------------------------------------------------------------------------------
rss:1022.02MB heapTotal: 7.75MB heapUsed: 2.05MB external: 1000.93MB arrayBuffers: 1000.01MB
......

这时候你会发现external和arrayBuffer的数据飞速数增长,而heapTotal和heapUsed几乎没有变化。这一方面说明了nodejs可以突破v8的堆内存限制,另一方面说明由Buffer分配的内存其底层是由C/C++模块实现,所以external的数据也跟随arrayBuffers一起发生了变化。

nodejs通过Buffer突破v8的堆内存是因为nodejs作为JavaScript的服务端环境,需要考虑网络流和文件I/O流对内存的需求。

查看系统的内存占用:

这里需要注意的是os需要使用require引入才能使用的模块。

console.log(os.totalmem());//系统总内存大小
console.log(os.freemem()); //当前可用内存大小

 四、内存泄漏与内存泄漏排查

在v8的垃圾回收机制下,一般情况的代码编写很少会出现内存泄漏的情况,但内存泄漏往往都是在无意之间产生的,较难排查。尽管内存泄漏的情况不尽相同,但其实质只有一个,那就是应用当回收的对象出现意外没有被回收,变成了常驻在老生代内存中的对象。

4.1通常造成内存泄漏的原因有这几种:缓存、队列消费不及时、作用域未释放。

缓存:

因为内存比I/O的效率高,一旦命中内存中的缓存,就可以节省一次I/O的时间,这是一种极具诱惑的把内存当缓存使用的缘由。还有就是在JavaScript中直接使用对象的键值对来缓存数据,这种便捷性也是把内存当缓存使用的重要原因。缓存中存储的键越多,长期存活的对象也就会越多,这将导致垃圾回收在进行垃圾回收和整理时,对这些对象做无用功。

虽然使用内存作为缓存有非常大的潜在风险,但有些功能中我们也不得不使用内存来作为缓存,只要我们在适当的时候释放这些缓存或小心使用也不是不可以,下面就通过缓存限制策略来实现一个简单的使用内存作为缓存的示例:

 1 //缓存限制策略
 2 const LimitableMap = function(limit){
 3     this.limit = limit || 10; //设置缓存对象的个数,默认为10个
 4     this.map = {};            //实现缓存的键值对
 5     this.keys = [];           //缓存键队列
 6 };
 7 const hasOwnProperty = Object.prototype.hasOwnProperty;
 8 LimitableMap.prototype.set = function(key, value){
 9     let map = this.map;
10     let keys = this.keys;
11     if(!hasOwnProperty.call(map,key)){
12         if(keys.length === this.limit){
13             //如果当前缓存的key超出最大值,清除缓存中最早添加的key及key的内存对象的引用
14             let firstKey = keys.shift();
15             delete map[firstKey];
16         }
17         keys.push(key); //如果没有key则添加
18     }
19     map[key] = value; //赋值,如果value被更新,会切断之前的内存对象引用,重新申请一片新的内存作为新值的内存对象空间,之前的值引用的内存对象会被回抽
20 };
21 LimitableMap.prototype.get = function(key){
22     return this.map[key];   //使用缓存
23 };
24 module.exports = LimitableMap;

测试代码:

1 const LimitableMap = require('./test.js');
2 let limitableBuffer = new LimitableMap(3);
3 limitableBuffer.set('a','aaa');
4 limitableBuffer.set('b','bbb');
5 limitableBuffer.set('c','ccc');
6 limitableBuffer.set('d','ddd');
7 console.log(limitableBuffer.get('a'), limitableBuffer.get('b'), limitableBuffer.get('c'), limitableBuffer.get('d'));
View Code

缓存解决方案:

缓存限制策略的淘汰策略并不高效,能应付一些小型的应用场景,如果需要更高效的缓存可以考虑Isaac Z. Schlueter采用LRU算法的缓存,guthub地址:https://github.com/isaacs/node-lru-cache

另外还可以考虑使用模块机制通过exports导出函数,nodejs为了加速模块的引入,所有模块都会通过编译执行,然后缓存起来,而模块是常驻老生代的,但这种设计需要小心内存泄漏。

总的来说,直接使用进程内的内存作为缓存的方案都存在内存泄漏的风险,需要非常谨慎。如果有大量缓存的需求,还是建议使用进程的外部缓存软件,外部缓存软件有着良好的缓存过期淘汰策略及自右内存管理,不影响Node进程的性能。

使用外部缓存软件的优势:

-- 将缓存转移到外部,减少常驻内存的对象数量,让垃圾回收更高效。

-- 进程之间可以共享缓存。

目前市面上比较好的缓存软件有:Redis和Memcached。

队列状态:

JavaScript中使用队列来完成许多特殊需求,比如Bagpipe。队列在消费者——生产者模型中充当中间产物,当消费速度小于生产速度就会形成堆积,造成内存泄漏。

比如在收集日志的时候,如果欠考虑也许会采用数据库记录日志,日志通常是海量的,而数据库构建在文件系统上,写入效率远远低于文件直接写入,于事会形成数据库写入操作的堆积,而JavaScript中相关的作用域也不会得到释放,从而导致内存泄漏。遇到这种情况选择用文件写入替换数据库会更高效,但如果生产速度因为某些原因突然激增,或者消费速度因为故障降低,内存还是会可能出现泄漏的风险。

关于队列堆积的情况解决方案,一方面可以通过监控队列的长度,一旦堆积通过监控系统产生报警并通知相关人员解决;另一方面则是任意异步调用都应该包含超时机制,一旦在限定的时间内未完成响应,通过回调函数传递超时异常,使得任意异步调用的回调都具备可控的响应时间,给消费速度一个下限。

对于Bagpipe而言,它提供了超时模式和拒绝模式。超时响应超时错误,当队列堆积时则对新到来的直接响应拥塞错误,这两种模式都有效的防止了队列拥塞的内存泄漏问题。

4.2内存泄漏排查

内存泄漏排查的常见工具:

v8-profiler:由Danny Coates提供,对v8堆内存抓取快照和对CPU进行分析,但很长时间没有更新了。

node-heapdump:Nodejs核心贡献者之一Ben Noordhuis编写的模块,它允许对v8堆内存抓取快照,用于时候分析。

node-mtrace:由Jimb Esser提供,它使用GCC的mtrace工具来分析内存泄漏。

dtrace:在Joyent的SmartOS系统上,有完善的dtrace工具用来分析内存泄漏。

node-memwatch:来自Mozilla的Lloyd Hilaiel贡献的模块,采用WTFPL许可发布。

4.2.1node-heapdump:

相关使用方法参考github官方文档:https://github.com/bnoordhuis/node-heapdump

安装注意事项:首先要确定当前设备安装了python环境、visual C++ Build Tools,如果没有安装的花我个人建议自己到官网手动下载安装即可,如果嫌麻烦可以通过下面这个命令以管理员的方式安装:

npm install --g --production windows-build-tools

由于visual C++ Tools的相关安装耗时较长,需要耐心的等,不要随便中断安装操作,否则会导致重复安装多个版本。然后还需要确定node全局下是否安装了node-gyp。

node-gyp -v    //检查是否安装了node-gyp
npm install node-gyp -g    //如果没有安装node-gyp,就先安装
npm heapdump    //最后在当前项目下安装node-heapdump

确定上面的工具都安装成功后,测试生成垃圾回收快照:

let heapdump = require('heapdump');
heapdump.writeSnapshot(function(err, filename) {
    console.log('dump written to', filename);
  });

测试生成快照这里我这里出现了一些暂时没有解决的问题,官方文档中给出了writeSnapshot两种传参方式,上面这一种我能测试成功在当前项目目录下生了垃圾回收快照文件,但下面这种方式无法生成。

heapdump.writeSnapshot('/var/local/' + Date.now() + '.heapsnapshot');

关于传参问题暂时不研究,毕竟我是在windows系统下测试,真正的在服务端环境不一致,只要当前有一种方式可以实现就测试学习如何用node-heapdump排查内存泄漏。当然官方文档中还给出了Linux的快照生成方式,启动项目然后向进程发送 SIGUSR2 信号来强制创建快照:

kill -USR2 <pid>

最后就是将生成的.heapsnapshot快照文件用Chrome浏览器的dev tool,选中Memory然后点击load按钮打开刚刚生成的.heapsnapshot快照文件,查看内存的情况排查内存泄漏:

 

 然后就会进入到快照文件的详细数据界面:

 

 关于具体如何分析这些数据就不在这里赘述了,如果有其他无法解决的问题就上网搜一下,资料还是挺多的,这里给一篇比较详细的介绍操作说明链接:https://blog.csdn.net/cc18868876837/article/details/116714814

4.2.2node-memwatch:

相关使用方法参考github官方文档:https://github.com/airbnb/node-memwatch

npm install @airbnb/node-memwatch

同样需要注意设备上需要安装python环境、visual C++ Build Tool相关环境和工具,由于我的当前设备环境支持其他工作,修改起来很麻烦,就不演示了。安装相关环境和工具再粘贴一次:

npm install --g --production windows-build-tools //注意以管理员身份启动命令工具安装

官方文档还是蛮详细的,怎么用直接看官方文档就OK了。这里有一篇《深入浅出nodejs》的node-memwatch笔记博客https://blog.csdn.net/ki4yous/article/details/107872537可以参考学习,我之前也是参考这本生学习的。

 五、大内存应用

nodejs作为js的服务端运行环境,操作大文件式必然的,由于v8的内存限制,不能直接使用fs.readFile()和fs.writeFile()进行大文件的操作,所以在nodejs扩展了基于流的I/O操作模块stream,除了流的以外nodejs还有包含一系列操作I/O操作的其他模块如Buffer、pipe。

 这里不对这些模块和功能做具体分析,后面会有对应的博客更详细的内容,这篇博客主要是解析nodejs的内存管理机制,然后在这部分与内存相关的大文件处理场景唯一要说明的就是,v8的内存限制不适合对大文件直接做读写操作,而应该使用基于v8内存限制之外的独立内存(Buffer实现)和流的模式来处理大文件。

 1 //可以使用两个文件测试一下这段代码
 2 const fs = require('fs');
 3 
 4 let reader = fs.createReadStream('in.txt');
 5 let writer = fs.createWriteStream('out.txt');
 6 
 7 reader.on('data',function(chunk){
 8     writer.write(chunk);
 9 });
10 reader.on('end',function(){
11     writer.end();
12 });

由于读写模式固定,上面的代码可以使用管道pipe简化:

const fs = require('fs');

let reader = fs.createReadStream('in.txt');
let writer = fs.createWriteStream('out.txt');
reader.pipe(writer);

关于nodejs的内存管理机制就分析到这里结束了,这篇博客主要介绍了:

--v8内存限制及其原因:单线程

--v8的垃圾回收机制及其相关算法:分代式管理

--如何基于v8实现高效的内存逻辑:避免把内存作为缓存、谨慎处理闭包相关逻辑、避免高频的CPU消费业务逻辑,如果有相关业务尽量拆分成多个小的业务块组合处理、及时释放对象。

--内存泄漏的常见情况和解决方案:缓存(使用专业的缓存软件)、队列消费不及时(超时处理)、作用域未释放(规范编码模式)。

--内存泄漏排查:了解一些常见的内存泄漏排查工具。

--大文件应用:采用流的方式处理大文件。

 

posted @ 2022-03-11 15:08  他乡踏雪  阅读(3199)  评论(0编辑  收藏  举报