nodejs内存泄漏

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,且耗时较长。

 

 

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

内存泄漏类型
内存泄漏包含的类型有:常发性、偶发性、一次性、隐式。

常发性 
发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。

偶发性 
发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。

一次性 
发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块且仅一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。

隐式 
其主要是在调用函数或者模块时,当参数或者输入没有达到界定值时,是不会发生泄漏,当参数或者输入值达到一定时,才会发现内存泄漏,我们称这种为隐式。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天、几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。隐式才是我们本文中所需要去探索,去发现和解决的异常问题。
--------------------- 
作者:danhuang 
来源:CSDN 
原文:https://blog.csdn.net/dan_blog/article/details/50755117 
版权声明:本文为博主原创文章,转载请附上博文链接!

 

内存泄漏的途径

Node的内存构成主要是通过V8进行分配的部分和Node自行分配的部分。受V8的垃圾回收限制的主要是V8的堆内存。造成内存泄漏的主要原因:

1,全局缓存太多;

    

假定我们有模块leak.js

var leakArray = [];   
exports.leak = function () {  
  leakArray.push("leak" + Math.random());  
};

那么如果我们创建一个test.js来引用该模块,运行时,则会看的leakArray一直变大。

var Mod = require('./leak');
Mod.leak();
Mod.leak();
Mod.leak();
Mod.leak();
Mod.leak();

 

过大的数组循环 
先看下如下代码:

for ( var i = 0; i < 100000000; i++ ) {
    var user       = {};
    user.name  = 'outmem';
    user.pass  = '123456';
    user.email = 'outmem[@outmem](/user/outmem).com';
}

这段代码最主要的原因在于循环太大,直接内存分配到超过v8内存限制数量。由于JavaScript事件循环的执行机制,这段代码没有机会进入下一个事件循环。用setInterval和setTimeout可以进入下一个循环。但是不推荐用setInterval和setTimeout。对于大循环代码,建议最好是分割,然后进行处理,分段进行处理。因为每次都没有效利用好一次循环。一次事件循环,不要超过10ms。
 

2,队列消费不及时;

     简单的例子: 生产者一直在产生请求, 消费者处理太慢。

3,闭包

     类似C++的static。

4,循环引用

var func = function () {}
var el = function () {}
el.func = func;
func.element = el;
 

5.被忘记的 Timers 或者 callbacks

在 JavaScript 中使用 setInterval 非常常见。

大多数库都会提供观察者或者其它工具来处理回调函数,在他们自己的实例变为不可达时,会让回调函数也变为不可达的。对于 setInterval,下面这样的代码是非常常见的:

 
  1. var serverData = loadData();

  2. setInterval(function() {

  3. var renderer = document.getElementById('renderer');

  4. if(renderer) {

  5. renderer.innerHTML = JSON.stringify(serverData);

  6. }

  7. }, 5000); //This will be executed every ~5 seconds.

这个例子阐述着 timers 可能发生的情况:计时器会引用不再需要的节点或数据。

renderer 可能在将来会被移除,使得 interval 内的整个块都不再被需要。但是,interval handler 因为 interval 的存活,所以无法被回收(需要停止 interval,才能回收)。如果 interval handler 无法被回收,则它的依赖也不能被回收。这意味着 serverData——可能存储了大量数据,也不能被回收。在观察者模式下,重要的是在他们不再被需要的时候显式地去删除它们(或者让相关对象变为不可达)。

6.DOM 引用

有时候,在数据结构中存储 DOM 结构是有用的。假设要快速更新表中的几行内容。将每行 DOM 的引用存储在字典或数组中可能是有意义的。当这种情况发生时,就会保留同一 DOM 元素的两份引用:一个在 DOM 树种,另一个在字典中。如果将来某个时候你决定要删除这些行,则需要让两个引用都不可达。

 
  1. var elements = {

  2. button: document.getElementById('button'),

  3. image: document.getElementById('image')

  4. };

  5. function doStuff() {

  6. elements.image.src = 'http://example.com/image_name.png';

  7. }

  8. function removeImage() {

  9. // The image is a direct child of the body element.

  10. document.body.removeChild(document.getElementById('image'));

  11. // At this point, we still have a reference to #button in the

  12. //global elements object. In other words, the button element is

  13. //still in memory and cannot be collected by the GC.

  14. }

还有一个额外的考虑,当涉及 DOM 树内部或叶子节点的引用时,必须考虑这一点。假设你在 JavaScript 代码中保留了对 table 特定单元格(<td>)的引用。有一天,你决定从 DOM 中删除该 table,但扔保留着对该单元格的引用。直观地来看,可以假设 GC 将收集除了该单元格之外所有的内容。实际上,这不会发生的:该单元格是该 table 的子节点,并且 children 保持着对它们 parents 的引用。也就是说,在 JavaScript 代码中对单元格的引用会导致整个表都保留在内存中的。保留 DOM 元素的引用时,需要仔细考虑。

 

 

7.同一个事件 被监听多次

内存泄漏分析

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

1

2

3

4

5

6

process.memoryUsage();

  {

    ress: 47038464, 

    heapTotal: 34264656, 

    heapUsed: 2052866 

  }

ress:进程的常驻内存部分

heapTotal,heapUsed:V8堆内存信息

 

javascript的基本类型:Undefined,Null,Boolean,Number,String

引用类型:Object,Array,Function

基本类型值在内存中占据固定大小,被保存在栈内存中,引用类型值是对象,保存在堆内存中。

Javascript的内存的生命周期对于用户来说是透明的,不开放的。在定义变量时候就完成了分配内存,使用时候是对内存的读写操作,内存的释放依赖于浏览器的垃圾回收机制。

栈(stack)和堆(heap)==>

1,栈

stack是有结构的,先进后出,存放基本类型和对象的引用,每个区块的大小是明确的。

2,堆

heap没有结构,数据任意存放,js中主要存放的是引用类型,比如:Array,Object对象

所以明显看出:数据查询速度比较的话,stack远远大于heap。

在实际开发过程中,偶尔遇到栈溢出的情况,stack overflow错误,因为stack创建时候,大小是确定的,超过额度大小就会发生栈溢出【当js出现死循环或者错误的递归时候】。heap大小是不确定的,需要可以一直累加。

js是单线程的,核心特征哈,那么怎么利用多核的CPU呢?H5的Web Worker标准,允许js脚本创建多个线程,但是子线程受主线程的控制,且不能操作DOM。

stack是线程独占的,heap是线程共有的。

基本类型在当前执行环境结束时销毁,而引用类型不会随执行环境结束而销毁,只有当所有引用它的变量不存在时这个对象才被垃圾回收机制回收。

 

查看垃圾回收日志

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

node --prof  test.js              //输出node执行时性能日志。 然后用node --prof-process  XXXX(XXXX是日志文件)

以上两种都很难看。太专业,不适合看

 

 

分析监控工具

///
node-memwatch 监听垃圾回收情况

let memwatch = require("node-memwatch")

memwatch.on('stats',function(info){

  console.log(info)

})

memwatch.on('leak',function(info){

  console.log(info)

})

  1. 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反映垃圾回收情况。usage_trend标识是不是有泄漏的趋势

  1. leak事件:内存在连续 5 次 GC 后都是增长的

                  

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

以上只是用于判断可能有内存泄漏。

memwatch.HeapDiff(): 查找泄漏元凶

最后,node-memwatch能比较堆上对象的名称和分配数量的快照,其对比前后的差异可以帮助找出导致内存泄漏的元凶。

var hd = new memwatch.HeapDiff();
 
// Your code here ...
 
var diff = hd.end();

对比产生的内容就像这样:

{
  "before": {
    "nodes": 11625,
    "size_bytes": 1869904,
    "size": "1.78 mb"
  },
  "after": {
    "nodes": 21435,
    "size_bytes": 2119136,
    "size": "2.02 mb"
  },
  "change": {
    "size_bytes": 249232,
    "size": "243.39 kb",
    "freed_nodes": 197,
    "allocated_nodes": 10007,
    "details": [
      {
        "what": "Array",
        "size_bytes": 66688,
        "size": "65.13 kb",
        "+": 4,
        "-": 78
      },
      {
        "what": "Code",
        "size_bytes": -55296,
        "size": "-54 kb",
        "+": 1,
        "-": 57
      },
      {
        "what": "LeakingClass",
        "size_bytes": 239952,
        "size": "234.33 kb",
        "+": 9998,
        "-": 0
      },
      {
        "what": "String",
        "size_bytes": -2120,
        "size": "-2.07 kb",
        "+": 3,
        "-": 62
      }
    ]
  }
}

HeapDiff方法在进行数据采样前会先进行一次完整的垃圾回收,以使得到的数据不会充满太多无用的信息。memwatch的事件处理会忽略掉由HeapDiff触发的垃圾回收事件,所以在stats事件的监听回调函数中你可以安全地调用HeapDiff方法。 

具体可以看npm 中的介绍

这个方法也只能看到哪一种数据的变化,并不能看到细致的

//

node-heapdump 对v8堆内存抓取快照

样例代码:

//app.js

var app = require('express')();

var http = require('http').Server(app);

var heapdump = require('heapdump');

var leakobjs = [];

class LeakClass{

x:number = 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'); //这种方式是写死的写快照。还有一种方法就是注释掉这个。然后通过kill -USR2 pid来收集.当然也可以在memwatch检测到leak,就打印快照

}, 3000);

http.listen(3000, function(){

console.log('listening on port 3000');

});

 

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

 

v8-profiler 对v8堆内存抓取快照和对cpu进行分析

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

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

 

 

可以在线上随时拉取, 但是很难看懂

 



 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值