深刻浅出Node.js学习笔记(五)

内存控制

基于无阻塞、事件驱动创建的Node服务,具备内存消耗低的优势,很是适合处理海量的网络请求。node

1. V8的垃圾回收机制与内存限制

对于性能敏感的服务器程序,内存管理的好坏、垃圾回收情况是否优良,都会对服务构成影响。而在Node中,这一切都与Node的JavaScript执行引擎V8息息相关。算法

1.1 Node与V8

Node是一个构建在Chrome的JavaScript运行时上的平台。浏览器

V8的性能优点使得用JavaScript写高性能后台服务程序成为可能。缓存

Node在JavaScript的执行上直接受益于V8,能够随着V8的升级就能享受到更好的性能或新的语言特性,同时也受到V8的一些限制,例如内存限制。bash

1.2 V8的内存限制

在Node中经过JavaScript对使用内存就会发现只能使用部份内存(64位约1.4GB,32位约0.7G)。服务器

V8 的内存限制会致使Node没法直接操做大内存对象。网络

形成这个问题的主要缘由在于Node基于V8构建,因此在Node中使用的JavaScript对象基本上都是经过V8本身的方式来进行分配和管理的。闭包

1.3 V8的对象分配

在V8中,全部的JavaScript对象都是经过堆来进行分配的。ide

Node提供了V8中内存使用量的查看方式。函数

$ node
> process.memoryUsage()
{ rss: 22044672,
  heapTotal: 9682944,
  heapUsed: 5296232, //已申请的堆内存
  external: 8860 }   //已使用的堆内存
复制代码

在代码中声明变量并赋值时,所使用对象的内存就分配在堆中。若是已申请的堆空闲内存不够分配新的对象,将继续申请堆内存,知道堆的大小超过V8的限制为止。

V8为什么要限制堆的大小?

  • 表层缘由为V8最初为浏览器设计,不太可能遇到用大量内存的场景;
  • 深层缘由是V8的垃圾回收机制的限制;

打开限制使用更多的内存:

node --max-old-space-size=1700 test.js //单位为MB
//或者
node --max-new-space-size=1024 test.js //单位为KB
复制代码

1.4 V8的垃圾回收机制

V8用到的各类垃圾回收算法:

  1. V8主要的垃圾回收算法

    V8的垃圾回收策略主要基于分代式垃圾回收机制。

    现代的垃圾回收算法中按对象的存活时间将内存的垃圾回收进行不一样的分代,再分别对不一样分代的内存进行更高效的算法。

    • V8的内存分代

      在V8中,主要将内存分为新生代和老生代两代。

      新生代中的对象存活时间较短;

      老生代中的对象存活时间较长或常驻内存的对象;

      V8堆的总体大小就是新生代所用内存加上老生代的内存空间。

    • Scavenge算法

      在分代的基础上,新生代中的对象主要经过Scavenge算法进行垃圾回收。Scavenge的具体实现中,主要采用Cheney算法。

      Cheney算法是一种采用复制的方式实现的垃圾回收算法。

      在垃圾回收的过程当中,就是经过将存活对象在两个semisoace空间进行复制。

      Scavenge的缺点:只能使用堆内存的一半。

      Scavenge是典型的牺牲空间换取时间的算法,因此没法大规模地应用到全部的垃圾回收中。

      当一个对象通过屡次复制依然存活时,它将被认为是生命周期较长的对象。这种较长生命周期的对象随后会被移动到老生代中,采用新的算法进行管理。

      对象重新生代中移动到老生代中的过程称为晋升

    • Mark-Sweep & Mark-Compact

      Mark-Sweep是标记清除的意思,分为标记和清除两个阶段。

      Mark-Sweep最大的问题是在进行一次标记清除回收后,内存空间会出现不连续的状态。

      Mark-Compact是标记整理的意思,是在Mark-Sweep的基础上演变而来;

    • Incremental Marking

      为了不出现JavaScript应用逻辑与垃圾回收器看到不一致的状况,垃圾回收的3种基本算法都须要将应用逻辑暂停下来,待执行完垃圾回收再回复执行应用逻辑,这种行为被称为“全停顿”(stop-the-world)。

      为了下降全堆垃圾回收带来的停顿时间,V8先从标记阶段入手,将本来要一口气停顿完成的动做改成增量标记(Incremental Marking)。

1.5 查看垃圾回收日志

查看垃圾回收日志的方式主要是在启动时添加--trace_gc参数。在进行垃圾回收时,将会从标准输出中打印垃圾回收的日志信息。

经过分析垃圾回收日志,能够了解垃圾回收的运行情况,找出垃圾回收的哪些阶段比较耗时,触发的缘由是什么。

2. 高效使用内存

在V8面前,开发者所要具有的责任是如何让垃圾回收机制更高效地工做。

2.1 做用域

在JavaScript中能造成做用域的有函数调用、with以及全局做用域。

  1. 标识符查找

  2. 做用域链

  3. 变量的主动释放

    若是须要释放常驻内存的对象,能够经过delete操做来删除引用关系。或者将变量从新赋值,让旧的对象脱离引用关系。

    虽然delete操做和从新赋值具备相同的效果,可是在V8中经过delete删除对象的属性可能干扰V8的优化,全部经过赋值方式解除引用更好。

2.2 闭包

在JavaScript中,实现外部做用域访问内部做用域中的变量的方法叫作闭包(closure)。这得益于高阶函数的特性:函数能够做为参数或者返回值。

闭包是JavaScript的高级特性,利用它能够产生不少的巧妙的效果。一旦有变量引用这个中间函数,这个中间函数将不会释放,一样也会使原始的做用域不会获得释放,做用域中产生的内存占用也不会获得释放。除非再也不引用,才会逐步释放。

3. 内存指标

3.1 查看内存的使用状况

  1. 查看进程的内存占用

    调用process.memoryUsage()能够看到Node进程的内存占用状况。

    rss是resident set size的缩写,即进程的常驻内存部分。进程的内存总共有几部分,一部分是rss,其他部分在交换区(swap)或者文件系统(filesystem)中。

  2. 查看系统的内存占用

    OS模块中的totalmem()和freemem()用于查看操做系统的内存使用状况。

    分别返回系统的总内存和闲置内存,单位是字节。

    $ node
    > os.totalmem()
    8446971904
    > os.freemem()
    2469531648
    >
    复制代码

3.2 堆外内存

堆中的内存用量老是小于进程的常驻内存量,这意味着Node中的内存使用并不是都是经过V8进行分配的。将不是经过V8分配的内存称为堆外内存

堆外内存能够突破内存限制。

4. 内存泄露

Node对内存泄露十分敏感,一旦线上应用有成千上万的流量,哪怕是一个字节的内存泄露也会形成堆积,垃圾回收过程将会耗费更多时间进行对象扫描,应用响应缓慢,直到进程内存溢出,应用崩溃。

内存泄露的缘由:

  • 缓存;
  • 队列消费不及时;
  • 做用域未释放;

4.1 慎将内存当作缓存

缓存在应用的做用举足轻重,能够十分有效地节省资源。缓存的访问效率要比I/O的效率高,一旦命中缓存,就能够节省一次I/O的时间。

一旦一个对象被当作缓存来使用,就意味着它将会常驻在老生代中。缓存中存储的键越多,长期存活的对象也就越多,这将致使垃圾回收在进行扫描和整理时,对这些对象作无用功。

JavaScript开发者一般喜欢用对象的键值对来缓存东西,但这与严格意义上的缓存有区别,严格意义上的缓存有着完善的过时策略,而普通对象的键值对并无。

一个无心识形成的内存泄露的场景:memoize

memoize的原理是以参数做为键进行缓存,之内存空间换CPU执行时间。

  1. 缓存限制策略

    为了解决缓存中的对象永远没法释放的问题,须要加入一种策略来限制缓存的无限增加。

  2. 缓存的解决方案

    如何使用大量缓存,目前比较好的解决方案是采用进程外的缓存,进程自身不存储状态。

    外部的缓存软件有着良好的缓存过时淘汰策略以及自有的内存管理,不影响Node进程的性能。在Node中主要解决两个问题:

    • 将缓存转移到外部,减小常驻内存的对象的数量,让垃圾回收更加的高效;
    • 进程之间能够共享缓存;

    目前,市场上较好的缓存有Redis和Memcached。

4.2 关注队列状态

在大多数应用场景下,消费的速度远远大于生产的速度,内存泄露不易产生。可是一旦消费速度低于生产速度,将会造成堆积。

表层的解决方案是换用消费速度更高的技术。

深度的解决方案是监控队列的长度。

5. 内存泄露排查

1.node-heapunp

2.node-memwatch

6. 大内存应用

Node提供了stream模块用于处理大文件。

相关文章
相关标签/搜索