Node.js内存管理中的那些事

Node与V8

基本概念

V8是Node的JavaScript执行引擎,V8引擎实际是一个高性能虚拟机。Node在JavaScript的执行直接受益于V8,可以随着V8的升级就能享受更好的性能或新的语言特性(如ES5和ES6)

二者之间的关系

  • 大小限制说明 对于一般的后端开发语言,基本内存使用是没有限制的,但是在Node中通过javaScript使用内存时只能使用部分内存(64位系统下约为1.4G,32位系统下约为0.6G)
  • 限制的原因与特殊说明 Node基于V8构建,所以在Node中使用javaScript基本都是通过V8自己的方式进行分配和管理的。但是Node的内存并不完全是通过V8进行分配管理的。查看内存使用情况的时候,发现堆中的内存用量总是小于进程的常驻内存用量rss。Node中的内存使用并非都是通过V8进行分配的,还有一些不是通过V8进行分配的对象,我们称之为堆外内存,堆外内存文章末尾会有一个说明(例如Buffer对象就不同于其他对象,他不经过V8的内存分配机制,不会有堆内存的限制)
  • V8的对象分配 V8中,所有的javaScript对象都是通过堆来进行分配的。V8的堆内存包括heapToal(已经申请到的堆内存),heapUsed(当前使用的堆内存);我们在代码中声明变量并赋值的时候,所使用的对象的内存就分配在堆中。如果已申请的堆空闲内存不够分配新的对象,将继续申请堆内存,直到堆的大小超过V8的限制为止。

说明:基于V8这种限制将会导致Node无法操作大内存对象,也因此后来出现了buffer这种不受V8丢内存控制的堆外内存管理。

开发过程中的那些不好回收的内存(高效使用内存)

由于V8已经对内存做了限制,我们应该做到高效的使用内存,让垃圾回收机制更高效的工作,避免一些不容易回收内存的出现。

作用域

在JavaScript中,能形成作用域的有函数,with以及全局作用域。

  • 作用域举例最基本的内存回收过程 var a=function(){ var local={}; } 函数a在每次被调用的时候会创建对应的作用域,函数执行结束后,该作用域将会销毁。同时因为该作用域中声明的局部变量分配在该作用域上,随作用域的销毁而销毁。只被局部变量引用的对象存活周期较短。代码中,由于对象较小,将会分配在新生代的Form空间中。作用域失效后,局部变量local失效,其引用的对象将会在下次垃圾回收时被释放。
  • 作用域中的变量查找回收 JavaScript在执行时会查找变量定义在哪,最先查找的当前作用域,当前作用域没有,会向上级的作用域查找,直到最顶层全局作用域查到,如果没有最后返回undefine。
  • 变量的主动释放回收 如果变量是全局变量(通过var声明或定义在global变量上),全局作用域直到进程退出才能释放,这种情况将导致引用的对象常驻内存(常驻在老生代中)。这种需要释放常驻内存中的对象,可以使用delete操作来删除引用关系,或者将变量重新赋值,让旧对象脱离引用关系(也就是对象的引用即所占的内存空间原本指向某个变量现在指向空获未定义),这样在接下来的老生代内存 清 除和整理的过程中会被释放。 global.foo="i am gang"; console.log(global.foo);// i am gang delete global.foo; //或者重新赋值 global.foo=undefined;// or null console.log(global.foo);//undefined

说明:虽然两种方式都可以主动释放变量引用的对象(也就是那一小块内存),但是推荐大家使用重新赋值的方法,因为在V8中通过delete删除对象的属性有可能干扰V8的优化。

闭包

在javaScript中,实现外部作用域访问内部作用域中变量的方法叫做闭包(closure)。这得益于高阶函数的特性:函数可以作为参数或者返回值。闭包它实现了外部作用域访问内部作用域中变量的方法。这句话需要好好理解。

简单例子说明闭包 两段代码对比:

var A=function(){
    (function(){
        var local="局部变量";
    }());
    console.log(local); //local未定义异常
}

var B=function(){
    var C=function(){
        var local="局部变量";
        return function(){
            return local;
        };
    };
    var c=C();
    console.log(c()); //局部变量
};

分析第二段代码,函数C执行完成后,局部变量local会随着作用域的销毁而被回收。但是注意这里的特点是返回值是一个匿名函数,而且这个函数中具备了访问local的条件,后面的代码执行,外部作用域是无法直接访问local的,但是若要访问它,只要通过这个中间函数稍作周转即可。以上就是闭包的基本分析,现在能够更好的理解我画重点的那句话了吧。

对于闭包的详细介绍,大家可以看这篇文章“https://juejin.im/post/5cf2515af265da1b6720f627”(明天公众号会发闭包的详解与面试相关内容)

内存相关基本命令使用

V8中内存使用情况查看

$ node
> process.memoryUsage();
{
    rss:14958592,
    heapTotal:7195904,
    heapUsed:2821496
}

heapTotal:V8中已申请的堆内存

heapUsed:V8中当前使用的堆内存

rss:进程的常驻内存部分

查看系统的内存占用

$ node
> os.totalmem()
82132131
> os.freemem()
31273127

os.totalmem 操作系统的总内存

os.freemem 操作系统的闲置内存

堆外内存

查看v8内存使用情况,process.memoryUsage()的结果可以看到,V8堆中的内存用量总是小于进程的常驻内存用量rss,也就是说Node中的内存使用并非都是V8控制,还有一部分不是通过V8分配的(rss-heaptotal这部分),不通过V8分配的内存称之为堆外内存。

使用buffer每次构造200MB的内存,代码如下:

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

代码执行过程中,查看内存使用情况会发现到最后,V8的使用内存heapUsed和申请的内存heaptotal基本不变,而常驻内存rss在不断增加,可以看出buffer对象不同于其它对象,不经过V8内存分配机制,不会有堆内存的限制。后面的文章会对buffer进行详细的讲解。

内存泄露

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

内存泄漏的本质

应当回收的对象出现意外而没有被回收,变成常驻在老生代中的对象。

造成内存泄漏的原因

  • 作用域未释放
  • 队列消费不及时

原文发布于微信公众号 - 程序员成长指北(coder_growth)

原文发表时间:2019-06-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券