详解Nodejs内存治理

s运行的宿主环境不同,相应的对内存治理的要求也不一样,当宿主环境是浏览器时,由于网页的运行时间短,且只运行在用户的机器上(相当于分布式),即使内存使用过多或者存在一定的内存泄漏,也并不会对终端用户产生太大的影响。当宿主环境编程服务器(Node)时,情况就大不相同了,本身代码运行在固定的几台机器(集中式)上,而且运行的时间是长时间运行,一旦内存治理不好出现了内存膨胀甚至是内存泄漏的情况的话,就会出现服务器端响应时间变长甚至是服务crash的情况。

Nodejs是基于V8构建的,所以在Node中使用的JavaScript对象基本上(Buffer就不是)都是通过V8来进行分配和管理的。V8在占用内存大小上做了限制(64位操作系统,单个Node进程可使用的最大堆内存大小约为1.5GB)。即使服务器的内存很大,但是由于V8的这种限制,导致Node无法充分利用服务器的资源。即便如此,为什么V8要做这样的限制呢?做这样限制的原因其实是与垃圾回收机制相关,以1.5GB的垃圾回收堆内存堆为例,V8做一次小的垃圾回收需要50ms以上,做一次全量的垃圾回收甚至要1s以上,要知道垃圾回收过程中JavaScript线程是要处于暂停执行的状态,太长的暂行时间对于后端服务的性能是会产生较大影响的,所以出于这方面考虑,V8对堆内存做了限制。即便如此,V8还是提供了可以自定义堆内存大小的方式(--max-old-pace-size),old-space代表老生代、new-space代表新生代。

node --max-old-space-size=xxx index.js //单位为MB
// 之前还可以通过-max-new-space-size来定义新生代堆大小,现在已经不可以了

当由于内存泄漏导致服务器一直频繁重启的时候,建议先调大堆内存大小来为定位问题争取时间,毕竟服务响应慢总比直接返回错误页对于用户而言会更好接受一点。

为什么需要老生代和新生代?

老生代和新生代其实是分代式垃圾回收机制里面的不同的分代,因为没有一种垃圾回收算法能够胜任所有的场景,不同的对象生存周期其实需要不同的回收策略才能达到最好的效果,所以V8采用分代式垃圾回收机制,降对象按的存活时间进行不同的分代,然后对不同分代(新生代、老生代)的内存施以更适合也更好的算法。

新生代中的对象存活时间较短,而老生代中的对象存活时间较长甚至是常驻内存。基于此所以设计的新生代的内存普遍要比老生代的内存小很多,V8中新生代最大内存是32M(64位系统为例),老生代最大内存是1400MB。V8实际使用的堆内存大小是新生代+老生代所用内存之和(1432MB),但是V8最大值其实是比使用的内存对大小额外大了32M(1464MB)

新生代如何做垃圾回收?

新生代的采用名叫Scavenge的垃圾回收算法。在Scavenge的具体实现中,主要采用了Cheney算法,Cheney算法通过将新生代堆一分为二,一个使用(From semispace),一个空闲(To semispace)。创建对象的时候,现在From空间中进行分配,当需要进行垃圾回收时,就检查From空间中的存活对象,然后将存活的对象拷贝到To空间,同时清空From空间,并将From和To互换,整个垃圾回收过程中就是将存活对象在两个seispace之间进行复制。对于生命周期短的场景存活对象在整个对象中占比较小,所以Scavenge采用的是复制存活的对象,但是Scavenge只能利用堆内存一半的空间,这是典型的用空间换时间的体现。

当一个对象经过多次垃圾回收依然存活的话,就会被认为是生命周期较长的对象,一方面新生代堆比较小,另一方面重复复制生命周期长的对象也很没有效率,所以对于生命周期长的对象会被移到老生代中去。新生代对象移动到老生代有两个对象:1.对象是否是生命周期较长的对象(已经经历过垃圾回收)2.To空间使用占比是否超过了25%。限制25%的原因是由于垃圾回收完成后To会变成From,如果不做限制的话可能会出现From很快被用光的情况,出现频繁的垃圾回收,也会影响效率。

老生代如何做垃圾回收?

老生代由于存活对象占较大比重,不适合对存活对象进行操作,使用Scavenge算法就不太合适了,因此老生代采用了Mark-Sweep和Mark-Compact相结合的方式。

Mark-Sweep分为标记和清除两个阶段,在标记阶段遍历堆中所有对象,标记活着的对象,然后在清除阶段未被标记的对象将会被清除掉。Mark-Sweep解决了内存释放的问题但是由于没有像Scavenge那样复制对象的操作导致内存碎片化不连续。而Mark-Compact就是用来解决内存碎片化问题的。Mark-Compact会将存活的对象往一端移动,移动完成后直接清理掉边界外的内存,这样就有大段的连续可用内存了,但是由于涉及到对象的移动,因此Mark-Compact的速度要比Mark-Sweep慢了。V8主要使用Mark-Sweep,只有当空间不足以对新生代中今生过来的对象进行分配时才使用Mark-Compact。

垃圾回收过程中会导致应用程序暂停执行,由于新生代本身空间较小,且要复制的存活对象占比也少,因此即便执行全量垃圾回收也影响不大,但是老生代空间很大,存活对象也多,执行一次全量垃圾回收对于应用程序暂停会是一个比较长的时间,因此V8将老生的标记改成了增量更新的方式,使得标记和应用程序交替执行直到标记完成,然后垃圾回收再执行后面的清理工作。注意清理工作并不是增量的。

开发者可以指定强制垃圾回收吗?

答案是可以了,在启动node服务的时候使用--expose-gc flag

$ node --expose-gc file.js

这样全局对象上就有了执行垃圾回收的函数

global.gc();

推荐更安全的写法

function forceGC()
 if (global.gc) {
  global.gc();
 } else {
  console.warn('No GC hook! Start your program as `node --expose-gc file.js`.');
 }
}

最后给大家分享一下参考资料:

https://speakerdeck.com/addyosmani/javascript-memory-management-masterclass

http://www.jb51.net/article/140005.htm

https://www.xarg.org/2016/06/forcing-garbage-collection-in-node-js-and-javascript/

您可能感兴趣的文章:

  • nodeJs内存泄漏问题详解
时间: 2018-05-11

nodeJs内存泄漏问题详解

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

内存溢出和内存泄漏的详解及区别

内存溢出和内存泄漏的详解及区别 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory:比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出. 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光. memory leak会最终会导致out of memory! 内存溢出就是你要求分配的内存超出了系统能

Tomcat 检测内存泄漏实例详解

 Tomcat如何检测内存泄漏 一般情况下,如果我们重启web应用是通过重启tomcat的话,则不存在内存泄漏问题.但如果不重启tomcat而对web应用进行重加载则可能会导致内存泄漏,因为重加载后有可能会导致原来的某些内存无法让GC回收,例如web应用使用了JDBC,驱动会进行注册,当web应用停止时没有反注册就会导致内存泄漏. 看看是什么原因导致tomcat内存泄漏的.这个要从热部署开始说起,因为tomcat提供了不必重启容器而只需重启web应用以达到热部署的功能,其实现是通过定义一个Web

JavaScript中的垃圾回收与内存泄漏示例详解

前言 程序的运行需要内存.只要程序提出要求,操作系统或者运行时就必须供给内存.所谓的内存泄漏简单来说是不再用到的内存,没有及时释放.为了更好避免内存泄漏,我们先介绍Javascript垃圾回收机制. 在C与C++等语言中,开发人员可以直接控制内存的申请和回收.但是在Java.C#.JavaScript语言中,变量的内存空间的申请和释放都由程序自己处理,开发人员不需要关心.也就是说Javascript具有自动垃圾回收机制(Garbage Collecation). 一.垃圾回收的必要性 下面这段话

Java GC 机制与内存分配策略详解

Java GC 机制与内存分配策略详解 收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现 自动内存管理解决的是:给对象分配内存 以及 回收分配给对象的内存 为什么我们要了解学习 GC 与内存分配呢? 在 JVM 自动内存管理机制的帮助下,不再需要为每一个new操作写配对的delete/free代码.但出现内存泄漏和溢出的问题时,如果不了解虚拟机是怎样使用内存的,那么排查错误将是一项非常艰难的工作. GC(垃圾收集器)在对堆进行回收前,会先确定哪些对象"存活",哪些已经&quo

Java语言中的内存泄露代码详解

Java的一个重要特性就是通过垃圾收集器(GC)自动管理内存的回收,而不需要程序员自己来释放内存.理论上Java中所有不会再被利用的对象所占用的内存,都可以被GC回收,但是Java也存在内存泄露,但它的表现与C++不同. JAVA中的内存管理 要了解Java中的内存泄露,首先就得知道Java中的内存是如何管理的. 在Java程序中,我们通常使用new为对象分配内存,而这些内存空间都在堆(Heap)上. 下面看一个示例: public class Simple { public static vo

NodeJS自定义模块写法(详解)

如下所示: //1.创建测试模块js文件(我这里命名为test.js) //2.添加测试方法 function test(){ console.log('Test Success!'); } //3.公开该方法到node模块 //exports.test(这个是public的方法名,外部调用的时候,使用这个方法名) exports.test = test; //4.测试(在另一个js文件中引入这个模块,并调用对应测试函数,两个js文件在同一目录下) const testModule = requ

C语言动态内存分配的详解

C语言动态内存分配的详解 1.为什么使用动态内存分配 数组在使用的时候可能造成内存浪费,使用动态内存分配可以解决这个问题. 2. malloc和free C函数库提供了两个函数,malloc和free,分别用于执行动态内存分配和释放. (1)void *malloc(size_t size); malloc的参数就是需要分配的内存字节数.malloc分配一块连续的内存.如果操作系统无法向malloc提供更多的内存,malloc就返回一个NULL指针. (2)void free(void *poi

C++中的内存对齐实例详解

C++中的内存对齐实例详解 内存对齐 在我们的程序中,数据结构还有变量等等都需要占有内存,在很多系统中,它都要求内存分配的时候要对齐,这样做的好处就是可以提高访问内存的速度. 我们还是先来看一段简单的程序: 程序一 #include <iostream> using namespace std; struct X1 { int i;//4个字节 char c1;//1个字节 char c2;//1个字节 }; struct X2 { char c1;//1个字节 int i;//4个字节 ch

C++ 中继承与动态内存分配的详解

C++ 中继承与动态内存分配的详解 继承是怎样与动态内存分配进行互动的呢?例如,如果基类使用动态内存分配,并重新定义赋值和复制构造函数,这将怎样影响派生类的实现呢?这个问题的答案取决于派生类的属性.如果派生类也使用动态内存分配,那么就需要学习几个新的小技巧.下面来看看这两种情况: 一.派生类不使用new 派生类是否需要为显示定义析构函数,复制构造函数和赋值操作符呢? 不需要! 首先,来看是否需要析构函数,如果没有定义析构函数,编译器将定义一个不执行任何操作的默认构造函数.实际上,派生类的默认构造