nodeJs内存泄漏问题详解


Posted in NodeJs onSeptember 05, 2016

之前一次偶然机会发现,react 在server渲染时,当NODE_ENV != production时,会导致内存泄漏。具体issues: https://github.com/facebook/react/issues/7406 。随着node,react同构等技术地广泛运用,node端内存泄漏等问题应该引起我们的重视。为什么node容易出现内存泄漏以及出现之后应该如何排查,下面通过一个简单的介绍以及例子来说明。

首先,node是基于v8引擎基础上,其内存管理方式与v8一致。下面简单介绍v8的相关内存特效。

V8内存限制

node基于V8构建,通过V8的方式进行分配跟管理js对象。V8对内存的使用有限制(老生代内存64位系统下约为1.4G,32位系统下约为0.7G,新生代内存64位系统下约为32MB,32系统下约为16MB)。在这样的限制下,将导致无法操作大内存对象。如果不小心触碰这个界限,就会造成进程退出。

原因:V8在执行垃圾回收时会阻塞JavaScript应用逻辑,直到垃圾回收结束再重新执行JavaScript应用逻辑,这种行为被称为“全停顿”(stop-the-world)。若V8的堆内存为1.5GB,V8做一次小的垃圾回收需要50ms以上,做一次非增量式的垃圾回收甚至要1秒以上。

通过node --max-old-space-size=xxx(单位MB) , node --max-new-space-size=xxx(单位KB) 设置新生代内存以及老生代内存来破解默认的内存限制。

V8的堆构成

V8的堆其实并不只是由老生代和新生代两部分构成,可以将堆分为几个不同的区域:

  1. 新生代内存区:大多数的对象被分配在这里,这个区域很小但是垃圾回特别频繁
  2. 老生代指针区:属于老生代,这里包含了大多数可能存在指向其他对象的指针的对象,大多数从新生代晋升的对象会被移动到这里
  3. 老生代数据区:属于老生代,这里只保存原始数据对象,这些对象没有指向其他对象的指针
  4. 大对象区:这里存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收其不会移动大对象
  5. 代码区:代码对象,也就是包含JIT之后指令的对象,会被分配在这里。唯一拥有执行权限的内存区
  6. Cell区、属性Cell区、Map区:存放Cell、属性Cell和Map,每个区域都是存放相同大小的元素,结构简单

GC回收类型

增量式GC

表示垃圾回收器在扫描内存空间时是否收集(增加)垃圾并在扫描周期结束时清空垃圾。

非增量式GC

使用非增量式垃圾收集器时,一收集到垃圾即将其清空。

垃圾回收器只会针对新生代内存区、老生代指针区以及老生代数据区进行垃圾回收。对象首先进入占用空间较少的新生代内存。大部分对象会很快失效,非增量GC直接回收这些少量内存。假如有些对象一段时间内不能被回收,则进去老生代内存区。这个区域则执行不频繁的增量GC,且耗时较长。

那什么时候才会导致内存泄漏的发生呢?

内存泄漏的途径

  1. 内存泄露
  2. 缓存
  3. 队列消费不及时
  4. 作用域未释放

Node的内存构成主要是通过V8进行分配的部分和Node自行分配的部分。受V8的垃圾回收限制的主要是V8的堆内存。造成内存泄漏的主要原因:1,缓存;2,队列消费不及时;3,作用域未释放

内存泄漏分析

查看V8内存使用情况(单位byte)

process.memoryUsage(); 
  {
    ress: 47038464,  
    heapTotal: 34264656,  
    heapUsed: 2052866  
  }

ress:进程的常驻内存部分

heapTotal,heapUsed:V8堆内存信息

查看系统内存使用情况(单位byte)

os.totalmem()
os.freemem()

返回系统总内存以及闲置内存

查看垃圾回收日志

node --trace_gc -e "var a = []; for( var i = 0; i < 1000000; i++ ) { a.push(new Array(100)); }" >> gc.log  //输出垃圾回收日志

node --prof //输出node执行时性能日志。 使用windows-tick.processor查看。

分析监控工具

v8-profiler 对v8堆内存抓取快照和对cpu进行分析
node-heapdump 对v8堆内存抓取快照
node-mtrace 分析堆栈使用
node-memwatch 监听垃圾回收情况

node-memwatch

memwatch.on('stats',function(info){
  console.log(info)
})
memwatch.on('leak',function(info){
  console.log(info)
})

stats事件:每次进行全堆垃圾回收时,将触发一次stats事件。这个事件将会传递内存统计信息。

{
"num_full_gc": 17, //第几次全栈垃圾回收
"num_inc_gc": 8,  //第几次增量垃圾回收
"heap_compactions": 8, //第几次对老生代进行整理
"estimated_base": 2592568, //预估基数
"current_base": 2592568, //当前基数
"min": 2499912, //最小
"max": 2592568, //最大 
"usage_trend": 0 //使用趋势
  }

观察num_full_gc和num_inc_gc反映垃圾回收情况。

leak事件:如果经过连续5次垃圾回收后,内存仍然没有被释放,意味着内存泄漏的发生。这个时候会触发一个leak事件。

{ 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'
}

Heap Diffing 堆内存比较 排查内存溢出代码。
下面,我们通过一个例子来演示如何排查定位内存泄漏:

首先我们创建一个导致内存泄漏的例子:

//app.js
var app = require('express')();
var http = require('http').Server(app);
var heapdump = require('heapdump');

var leakobjs = [];
function LeakClass(){
  this.x = 1;
}

app.get('/', function(req, res){
  console.log('get /');
  for(var i = 0; i < 1000; i++){
    leakobjs.push(new LeakClass());
  }
  res.send('<h1>Hello world</h1>');
});

setInterval(function(){
  heapdump.writeSnapshot('./' + Date.now() + '.heapsnapshot');
}, 3000);

http.listen(3000, function(){
  console.log('listening on port 3000');
});

这里我们通过设置一个不断增加且不回被回收的数组,来模拟内存泄漏。

通过使用heap-dump模块来定时纪录内存快照,并通过chrome开发者工具profiles来导入快照,对比分析。

我们可以看到,在浏览器访问 localhost:3000 ,并多次刷新后,快照的大小一直在增长,且即使不请求,也没有减小,说明已经发生了泄漏。

nodeJs内存泄漏问题详解

接着我们通过过chrome开发者工具profiles, 导入快照。通过设置comparison,对比初始快照,发送请求,平稳,再发送请求这3个阶段的内存快照。可以发现右侧new中LeakClass一直增加。在delta中始终为正数,说明并没有被回收。

nodeJs内存泄漏问题详解

小结

针对内存泄漏可以采用植入memwatch,或者定时上报process.memoryUsage内存使用率到monitor,并设置告警阀值进行监控。

当发现内存泄漏问题时,若允许情况下,可以在本地运行node-heapdump,使用定时生成内存快照。并把快照通过chrome Profiles分析泄漏原因。若无法本地调试,在测试服务器上使用v8-profiler输出内存快照比较分析json(需要代码侵入)。

需要考虑在什么情况下开启memwatch/heapdump。考虑heapdump的频度以免耗尽了CPU。 也可以考虑其他的方式来检测内存的增长,比如直接监控process.memoryUsage()。

当心误判,短暂的内存使用峰值表现得很像是内存泄漏。如果你的app突然要占用大量的CPU和内存,处理时间可能会跨越数个垃圾回收周期,那样的话memwatch很有可能将之误判为内存泄漏。但是,这种情况下,一旦你的app使用完这些资源,内存消耗就会降回正常的水平。所以需要注意的是持续报告的内存泄漏,而可以忽略一两次突发的警报。

NodeJs 相关文章推荐
Nodejs使用mysql模块之获得更新和删除影响的行数的方法
Mar 18 NodeJs
Nodejs进程管理模块forever详解
Jun 01 NodeJs
nodejs导出excel的方法
Jun 30 NodeJs
nodejs开发——express路由与中间件
Mar 24 NodeJs
angular2+nodejs实现图片上传功能
Mar 27 NodeJs
nodejs模块nodemailer基本使用-邮件发送示例(支持附件)
Mar 28 NodeJs
Nodejs读取文件时相对路径的正确写法(使用fs模块)
Apr 27 NodeJs
nodejs处理图片的中间件node-images详解
May 08 NodeJs
nodejs实现超简单生成二维码的方法
Mar 17 NodeJs
NodeJS 中Stream 的基本使用
Jul 30 NodeJs
Nodejs技巧之Exceljs表格操作用法示例
Nov 06 NodeJs
NodeJS开发人员常见五个错误理解
Oct 14 NodeJs
浅谈Nodejs应用主文件index.js
Aug 28 #NodeJs
NodeJS远程代码执行
Aug 28 #NodeJs
用NodeJS实现批量查询地理位置的经纬度接口
Aug 16 #NodeJs
使用nodejs中httpProxy代理时候出现404异常的解决方法
Aug 15 #NodeJs
NodeJs的优势和适合开发的程序
Aug 14 #NodeJs
在windows上用nodejs搭建静态文件服务器的简单方法
Aug 11 #NodeJs
Nodejs抓取html页面内容(推荐)
Aug 11 #NodeJs
You might like
php $_SERVER[&quot;REQUEST_URI&quot;]获取值的通用解决方法
2010/06/21 PHP
PHP模拟asp中response类实现方法
2015/08/08 PHP
javascript 兼容鼠标滚轮事件
2009/04/07 Javascript
javascript 表格排序和表头浮动效果(扩展SortTable)
2009/04/07 Javascript
functional继承模式 摘自javascript:the good parts
2011/06/20 Javascript
用console.table()调试javascript
2014/09/04 Javascript
js 求时间差的实现代码
2016/04/26 Javascript
js removeChild 方法深入理解
2016/08/16 Javascript
Javascript 引擎工作机制详解
2016/11/30 Javascript
Angular.js初始化之ng-app的自动绑定与手动绑定详解
2017/07/31 Javascript
vue移动端监听滚动条高度的实现方法
2018/09/03 Javascript
pageGroup.js实现分页功能
2019/07/27 Javascript
vue表单数据交互提交演示教程
2019/11/13 Javascript
angular共享依赖的解决方案分享
2020/10/15 Javascript
[01:13:18]Secret vs Infamous 2019国际邀请赛淘汰赛 败者组 BO3 第一场 8.23
2019/09/05 DOTA
python采用django框架实现支付宝即时到帐接口
2016/05/17 Python
Python实现的爬虫功能代码
2017/06/24 Python
Django与JS交互的示例代码
2017/08/23 Python
解决phantomjs截图失败,phantom.exit位置的问题
2018/05/17 Python
python中for循环输出列表索引与对应的值方法
2018/11/07 Python
python3爬虫学习之数据存储txt的案例详解
2019/04/24 Python
Python向excel中写入数据的方法
2019/05/05 Python
Python实现检测文件的MD5值来查找重复文件案例
2020/03/12 Python
如何在windows下安装Pycham2020软件(方法步骤详解)
2020/05/03 Python
解决Tensorflow2.0 tf.keras.Model.load_weights() 报错处理问题
2020/06/12 Python
python爬虫scrapy框架之增量式爬虫的示例代码
2021/02/26 Python
Kipling凯浦林美国官网:世界著名时尚休闲包袋品牌
2016/08/24 全球购物
白宫黑市官网:White House Black Market
2016/11/17 全球购物
请编程遍历页面上所有 TextBox 控件并给它赋值为 string.Empty
2015/12/03 面试题
社区工作感言
2014/02/21 职场文书
电子银行营销方案
2014/02/22 职场文书
干部竞争上岗演讲稿
2014/09/11 职场文书
2015年底工作总结范文
2015/05/15 职场文书
匿名信格式范文
2015/05/27 职场文书
浅谈Redis存储数据类型及存取值方法
2021/05/08 Redis
MySQL空间数据存储及函数
2021/09/25 MySQL