关于Node.js中内存管理的思考与实践

本人最近沉迷于Node的探索,也对Node用于后端开发感兴趣。因此本着探索的精神,认真重读了一下《深刻浅出Node.js》关于内存的部分,以为本身其实开始不会去关心了解这些内容,如今真的开阔个人眼界,受益不浅。node

01 前言

随着Node的发展,JavaScript的运行已经再也不局限于运行在浏览器中了,Node在服务端的应用使得愈来愈多的问题也显现出来。对于刚接触JavaScript的开发者来讲,基本上不多会想到内存分配或者内存泄露的问题,为了跟上潮流,就讲解一下Node是如何合理高效地使用内存,防止内存泄漏。web

02 V8垃圾回收与内存限制

JavaScript编程不用关心内存的分配和释放的问题,由于它有垃圾回收机制来管理。可是咱们对于内存管理更深一步的了解可谓是不多,到底内存是怎么分配的、垃圾回收是怎么运行的?算法

Node与V8

咱们先来了解一下这二者是什么关系。首先V8是谷歌开发出来的浏览器引擎,性能可谓是十分优异,使得JavaScript编写服务器程序成为可能。V8的由来是虚拟机专家Lars Bak开发出来的,开发者以前的工做一直都是专一于高性能的虚拟机开发。编程

Node是一个构建在Chrome的JavaScript运行时平台,由此一来,Node的高性能就如同抱住了V8的大腿,能够随着V8的升级而享受更好的性能。后端

V8的内存限制

Node经过JavaScript使用内存的话会有限制,并且Node没法直接操做大内存对象。64位系统下约为1.4GB,而32位系统下约为0.7GB。缘由是Node基于V8构建,它的内存分配和管理都是由V8来控制,虽然在浏览器下这种分配机制没有问题,可是在Node中倒是有问题的。浏览器

V8的对象分配

在V8中,全部的JavaScript对象都是经过堆来分配的,对象占用的内存空间大小是不肯定的。 在这里插入图片描述服务器

当咱们在代码里写上一些变量的时候,咱们就会向堆中的内存空间进行申请。咱们上面说过V8是有内存分配限制的,这是由于V8的垃圾回收机制影响。咱们试想一下假若有1.5GB的垃圾须要等待回收,而V8的垃圾回收机制运行一次须要每50毫秒以上,在这段时间内,程序会没法运行。闭包

假如说我非要打开这个限制也不是不行,咱们能够在Node的启动时候更改一下参数便可:编辑器

node --max-old-space-size=1700   xxx.js    //单位是MB
node --max-new-space-size=1024  xxx.js    //单位是KB
复制代码

上面代码的意思就是新生代和老生代空间的更改,稍后解析什么是新生代和老生代空间。函数

V8垃圾回收机制

在V8中,咱们知道内存空间能够分为新生代和老生代。新生代空间主要是保存一些存活时间较短的对象,而老生代空间主要存储一些存活时间较长的对象。

以前说过64位系统只能使用约1.4GB,32位只能使用0.7GB的空间。这空间是包含新生代和老生代空间,且老生代的空间比新生代要多。 在这里插入图片描述

V8主要采用两种不一样的算法,分别做用于新生代和老生代,由于二者的关系不同。主要有类算法,第一是Scavenge算法,第二是Mark-Sweep & Mark-Compact。新老生代的特色以下:

新生代:存活对象较少

老生代:存活对象较多

①Scavenge算法

  • 把新生代空间一分为二,为From空间和To空间
  • 内存先分配到From空间,垃圾回收会检查From空间存活状况
  • 把From空间的存活对象移到To空间,释放From空间
  • 把From和To空间对换

这就完成了垃圾回收的一次清理过程。 在这里插入图片描述

②Mark-Sweep算法

  • 遍历堆中的全部对象,标记存活的对象
  • 清除没有被标记的对象 在这里插入图片描述

因为老生代空间中,存活的对象较多,使用 Scavenge 算法的话遍历的时间会相对来讲比较长,并且还会浪费一半的空间来存放对象,效率比较低。总的来讲,Scavenge 算法复制存活的对象,而 Mark-Sweep 算法清除死亡的对象。

③Mark-Compact算法

咱们看到上面这种算法回收一次事后产生了零碎的内存空间,假如这时候进来一个比较大的内存对象就没法完成分配,提早触发新一轮的垃圾回收机制。因此基于这个问题,在原来算法的基础上,采用压缩的办法,在标记活对象的过程当中,对象会往一边移动。而后标记完成以后直接清除边界的内存。 在这里插入图片描述

④Incremental Marking算法

与前面三种不同的是,这种算法**采用了“步进”的方式**进行,由于垃圾回收机制运行一次的时间也是有的,对于垃圾回收机制运行时间过长,那么页面的应用逻辑都要停下来等待,这种影响也是比较大的。

对于新生代来讲不用担忧这个问题,由于新生代存活对象比较少,内存占用也比较小。可是对于老生代来讲,对象比较多也比较大,停顿形成的影响大,因此必需要让应用逻辑受垃圾回收的影响小一点。

所以采用“步进”的方式,垃圾回收机制和逻辑代码**分段执行**,缓解停顿时间过长而致使应用逻辑执行不了的问题。 在这里插入图片描述

03 内存的使用

关于内存的话咱们就不得不聊一下做用域的问题了。咱们所写的代码中,能够造成做用域的无非就是几个方面,有函数、with以及全局做用域。

var A = function(){
  var user = {}
};
A();
复制代码

咱们在执行这个函数的时候就会建立一个函数做用域,也会建立一个局部的变量user。这个user只能在函数做用域里面使用,函数执行完毕以后做用域销毁,同时对象也失去引用,其引用的对象下次的垃圾回收时就会释放。这里的user是一个小对象,就会分配在新生代的from空间中。

咱们知道做用域会有一个做用域链的概念,就是在当前的做用域找不到变量以后就会向父级的做用域寻找,一直向外扩散,找不到就会抛出未定义的错误。

小结:

  • 闭包没有及时释放会形成内存泄漏,常见的有 定时器
  • 全局变量,此种变量须要进程退出才会被释放,引用的对象就会在 常驻内存中( 老生代

V8堆内存相关的指标

咱们能够调用process.memoryUsage()查看Node进程的内存使用状况。

heapTotal(堆中总共申请的内存) 和 heapUsed(堆中使用的内存) 表明 V8 的内存使用状况。 external 表明 V8 管理的,绑定到 Javascript 的 C++ 对象的内存使用状况。 rss 是常驻空间大小, 是给这个进程分配了多少物理内存(占总分配内存的一部分),包含全部的 C++ 和 JavaScript 对象与代码。

上面咱们说过老生代的内存空间大概在1400MB左右,为了验证一下,不惜代价搞了一波内存泄漏的操做,看一下到底内存是怎么被消耗掉的。咱们手动构造了一个全局的对象,让它放在常驻内存中,也就是老生代中。

function showMemory(j) {
  console.log(`这是第${j+1}次运行`)
  var mem = process.memoryUsage();
 function format(bytes) { return (bytes / 1024 / 1024).toFixed(2) + "MB"; }  console.log( `Process:总共申请的内存heapTotal:${format(mem.heapTotal)}, 目前堆中使用内存heapUsed:${format( mem.heapUsed )},常驻内存rss:${format(mem.rss)}` ); console.log( "======================================================================================" ); }  //吃人函数 function useMem() { var size = 20 * 1024 * 1024; var arr = new Array(size); for (var i = 0; i < size; i++) { arr[i] = 0; } return arr; }  var total = [];  for (var j = 0; j < 20; j++) {//运行20次,实际上也不会达到,由于会内存耗尽 showMemory(j); total.push(useMem()); }  showMemory(j) 复制代码

结果也是很是的amazing啊,运行不到10次内存就消耗完了。你们能够想象一下假如你的内存有泄露的显现是多么可怕,最终就会致使整个进程的退出了。这里咱们能够清楚地看到,老生代的空间大概是1400MB左右,由于假如再执行一次就会超出范围了。

堆外内存

上面咱们测试的是堆中的内存,可是咱们发现最后一次总共申请的内存只有1290.33MB,可是堆中却占据了1300.81MB,那么多出来的那部分是哪里的呢?没错,其实就是堆外内存。这部分的内存是不受V8控制的。

咱们改变一下上面的“吃人”函数:

function useMem() {
  var size = 200 * 1024 * 1024;
  var buffer = new Buffer(size);
  for (var i = 0; i < size; i++) {
    buffer[i] = 0;
  }
  return buffer;
}
复制代码

咱们发现循环的21次都成功运行,并且咱们看到常驻内存那里已经超过了V8的限制。这里的buffer对象是不受V8控制和分配的,属于堆外内存。

Node的内存主要是经过V8进行分配和Node自行分配的部分。可是受V8垃圾回收限制的主要是V8的堆内存。

04 小结

Node将JavaScript的主要应用扩展到了服务器端,因此咱们考虑的细节也要和浏览器的不一样,更多的是对内存资源的分配问题,稍有不慎可能会写出一些内存泄漏的问题,使得垃圾回收机制不能清理释放内存,严重可能会致使服务器崩溃。

因此平时咱们写代码也要谨慎一点,不要写全局变量或者频繁使用闭包,这些若是没有正确释放内存的话也会致使内存泄漏。内存泄漏的本质其实就是应当回收的对象可是却没有被回收(转移到老生代空间)。

参考书籍:

  • 深刻浅出Node.js
相关文章
相关标签/搜索