内存溢出与内存泄露

内存泄露:memory leak,是指程序在申请内存后,没有释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
内存溢出:out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

内存泄露最终会导致内存溢出.

内存泄漏

指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),
而系统也不能再次将它分配给需要的程序。一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出!比方说栈,栈满时再做进栈必定产生空间溢出,
叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出.

分类

内存泄漏可以分为4类:

  1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
  2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。
    对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
  3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。
    比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
  4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。
    但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。
真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,
因为较之于常发性和偶发性内存泄漏它更难被检测到。

识别内存泄漏

经验法则是,如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。这就要求实时查看内存占用。

  1. Node 提供的process.memoryUsage方法。
    1
    2
    3
    4
    5
    console.log(process.memoryUsage());
    //{ rss: 54656, 所有内存占用,包括指令区和堆栈。
    // heapTotal: 646546, "堆"占用的内存,包括用到的和没用到的。
    // heapUsed: 64654, 用到的堆的部分。
    // external: 6565 } V8 引擎内部的 C++ 对象占用的内存。

判断内存泄漏,以heapUsed字段为准。

  1. WeakSet 和 WeakMap
    ES6 新的数据结构:WeakSet 和 WeakMap。它们对于值的引用都是不计入垃圾回收机制的,所以名字里面才会有一个”Weak”,表示这是弱引用。
    测试WeakMap:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    // 手动执行一次垃圾回收,保证获取的内存使用状态准确
    > global.gc(); undefined // 查看内存占用的初始状态,heapUsed 为 4M 左右
    > process.memoryUsage();
    {rss: 21106688,
    heapTotal: 7376896,
    heapUsed: 4153936,
    external: 9059 }
    > let wm = new WeakMap();
    undefined
    > let b = new Object();
    undefined
    > global.gc();
    undefined
    // 此时,heapUsed 仍然为 4M 左右
    > process.memoryUsage();
    { rss: 20537344,
    heapTotal: 9474048,
    heapUsed: 3967272,
    external: 8993 }
    // 在 WeakMap 中添加一个键值对,
    // 键名为对象 b,键值为一个 5*1024*1024 的数组
    > wm.set(b, new Array(5*1024*1024));
    WeakMap {}
    // 手动执行一次垃圾回收
    > global.gc();
    undefined
    // 此时,heapUsed 为 45M 左右
    > process.memoryUsage();
    { rss: 62652416,
    heapTotal: 51437568,
    heapUsed: 45911664,
    external: 8951 }
    // 解除对象 b 的引用
    > b = null;null
    // 再次执行垃圾回收
    > global.gc();
    undefined
    // 解除 b 的引用以后,heapUsed 变回 4M 左右
    // 说明 WeakMap 中的那个长度为 5*1024*1024 的数组被销毁了
    > process.memoryUsage(); { rss: 20639744, heapTotal: 8425472, heapUsed: 3979792, external: 8956 }

只要外部的引用消失,WeakMap 内部的引用,就会自动被垃圾回收清除。

##内存溢出
就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
Node.js做密集型运算,或者所操作的数组、对象本身较大时,容易出现内存溢出的问题,这是由于Node.js的运行环境-V8引擎导致的。
如果经常有较大数据量运算等操作,需要对Node.js运行环境限制有充分的了解。

常遇到的两类内存溢出问题

  1. 密集型运算
    当需要一个较大的for循环来操作所有的数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    for (var i = 0; i < 10000000; i++) {
    ((i) => {
    var obj = {};
    obj.name = 'pooky';
    obj.domain = 'pooky.oicp.io';
    // 这里是一个保存或更新等操作
    setTimeout(()=>{
    console.log(i, obj);
    }, 0)
    })(i)
    }

每次运算所需的内存量并不大,但由于for循环,造成V8内存不能及时释放。随着程序运行时候的增加,内存占用量会越来越大,并最终导致内存的溢出。

  1. 操作数据量较大
    对象需要频繁的创建/销毁,或操作对象本身较大
    1
    2
    3
    4
    5
    6
    7
    8
    var arrs = [];
    for (var x=0;x<10000;x++){
    var arr=[];
    for (var y=0;y<10000;y++){
    arr = [y, 'pooky', 'pooky.oicp.io'];
    arrs.push(arr);
    }
    }

可能所创建对象本身并没有超过内存限制。但是除对象本身外:创建对象、对象引用、Node.js程序本身等都需要内存空间,这样就很容易导致内存的溢出。

内存溢出原因

  1. V8本身分配的内存较小、2. JavaScript语言本身限制、3. 程序员使用不当。

    V8对每个进程分配的运行内存,在32位系统中约为700MB,而在64位系统中约为1.4GB。

解决方法

  1. 使用process.nextTick()防止事件堆积
    process.nextTick()会在本次事件循环结束后,立即开始下次事件循环。这样可以使V8获得内存回收的机会,有效解决过多事件堆积造成的内存溢出。
    但这样做必然会造成运行效率的降低,而应该在速度在安全之间平衡,控制好循环的安全次数。

  2. 增加V8内存空间
    Node.js提供了一个程序运行参数–max-old-space-size,可以通过该参数指定V8所占用的内存空间,这样可以在一定程度上避免程序内存的溢出。

    1
    node --max-old-space-size=4096 app
  3. 使用非V8内存
    Node.js程序所使用的内存分为两类:

  • V8内存:数组、字符串等JavaScript内置对象,运行时使用“V8内存”
  • 系统内存:Buffer是一个Node.js的扩展对象,使用底层的系统内存,不占用V8内存空间。与之相关的文件系统fs和流Stream流操作,都不会占用V8内存。
    在Node中,使用Buffer可以读取超过V8内存限制的大文件。原因是Buffer对象不同于其他对象,它不经过V8的内存分配机制。