IT共论NodeJS技术栈我爱编程

深入理解Node.js垃圾回收与内存管理

2016-12-08  本文已影响2814人  写Blog不取名

使用JavaScript进行前端开发时几乎完全不需要关心内存管理问题,对于前端编程来说,V8限制的内存几乎不会出现用完的情况,但是由于后端程序往往进行的操作更加复杂,并且长期运行在服务器不重启,如果不关注内存管理,导致内存泄漏,就算1TB,也会很快用尽。
Node.js构建于V8引擎之上,因此本文首先讲解V8引擎的内存管理机制,了解底层原理后,再讲解Node开发中的内存管理与优化。

一、V8的内存管理机制

1.1 内存管理模型

Node程序运行中,此进程占用的所有内存称为常驻内存(Resident Set)。

除堆外内存,其余部分均由V8管理。

通过process.memoryUsage()可以查看此Node进程的内存使用状况:

内存使用状况
rss是Resident Set Size的缩写,为常驻内存的总大小,heapTotal是V8为堆分配的总大小,heapUsed是已使用的堆大小。可以看到,rss是大于heapTotal的,因为rss包括且不限于堆。

1.2 堆内存限制

默认情况下,V8为堆分配的内存不超过1.4G:64位系统1.4G,32位则仅分配0.7G。也就是说,如果你想使用Node程序读一个2G的文件到内存,在默认的V8配置下,是无法实现的。不过我们可以通过Node的启动命令更改V8为堆设置的内存上限:

//更改老年代堆内存
--max-old-space-size=3000 // 单位为MB
// 更改新生代堆内存
--max-new-space-size=1024 // 单位为KB

堆的内存上限在启动时就已经决定,无法动态更改,想要更改,唯一的方法是关闭进程,使用新的配置重新启动。

1.3 V8的垃圾回收机制

垃圾回收机制演变至今,已经出现了数种垃圾回收算法,各有千秋,适用于不同场景,没有一种垃圾回收算法能够效率最优于所有场景。因此研发者们按照存活时间长短,将对象分类,为每一类特定的对象,制定其最适合的垃圾回收算法,以提高垃圾回收总效率。

二、Node开发中的内存管理与优化

2.1 手动变量销毁

当任一作用域存活于作用域栈(作用域链)时,其中的变量都不会被销毁,其引用的数据也会一直被变量关联,得不到GC。有的作用域存活时间非常长(越是栈底,存活时间越长,最长的是全局作用域),但是其中的某些变量也许在某一时刻后就没有用处了,因此建议手动设置为null,断开引用链接,使得V8可以及时GC释放内存。
注意,不使用var声明的变量,都会成为全局对象的属性。前端开发中全局对象为window,Node中全局对象为global,如果global中有属性已经没有用处了,一定要设置为null,因为全局作用域只有等到程序停止运行,才会销毁。
Node中,当一个模块被引入,这个模块就会被缓存在内存中,提高下次被引用的速度。也就是说,一般情况下,整个Node程序中对同一个模块的引用,都是同一个实例(instance),这个实例一直存活在内存中。所以,如果任意模块中有变量已经不再需要,最好手动设置为null,不然会白白占用内存,成为“活着的死对象”。

2.2 慎用闭包

function outer(){
    var x = 1; // 真正的局部变量:outer执行完后立即死亡
    var y = 2; // 上下文变量:闭包死亡后才会死亡
    // 返回一个闭包
    return function(){
      console.log(y); // 使用了外层函数的变量 y
    }
}
var inner = outer(); // 通过inner变量持有闭包

有不少开发者认为,如果闭包被引用,那么闭包的外部函数也不会被释放,其中的所有变量都不会被销毁,比如我通过inner变量持有了闭包,此时outer中的 x、y 均活在内存中,不会被销毁。事实真是这样吗?
答案是:在V8的实现中,当outer执行完毕,x 立即死亡,仅有 y 存活
V8是这么做的:
当程序进入一个函数时,将会为这个函数创建一个上下文(Context),初始状态这个Context是空的,当读到这个函数(outer)中的闭包声明时,将会把此闭包(inner)中使用的外部变量,加入Context。在上面的例子中,由于inner函数使用了变量 y ,因此会将 y 加入Context。outer内部所有的闭包,都会持有这个Context


每一个闭包都会引用其外部函数的Context,以此访问需要读取的外部变量。被闭包捕捉,加入Context中的变量,我们称为Context变量,分配在堆。而真正的 局部变量(local variable)是 x ,保存在栈,当outer执行完毕后,其信息出栈,变量 x 自然销毁,而Context被闭包引用,如果有任何一个闭包存活,Context都将存活,y 将不会被销毁。
举一反三,再来看一个更复杂的例子:
function outer () { 
    var x; // 真正的局部变量
    var y; // context variable, 被inner1使用
    var z; // context variable, 被inner2使用
    function inner1 () { 
      use(y); 
    } 
    function inner2 () { 
      use(z); 
    } 
    function inner3 () { 
      /* 虽然函数体为空,但是作为闭包,依旧引用outer的Context */
    } 
    return [inner1, inner2, inner3];
}

x、y、z 三个变量何时死亡?
x 在outer执行完后立即死亡, y、z 需要等到inner1、inner2、inner3三个闭包都死亡后,才会死亡。
x 未被任何闭包使用,因此是一个真正的局部变量,保存在栈,函数执行完即被出栈死亡。由于 y、z 两个变量分别被inner1、inner2使用,则它们会被加入outer的Context。所有闭包都会引用外部函数的Context,即使inner3为空,不使用任何外部函数的变量,也会引用Context,所以需要等到三个闭包都死亡后,y、z 才会死亡。


因此:如果较大的对象成为了Context变量,建议严格控制引用此Context的闭包生命周期以及闭包数量,或在不需要时,设置为null,以免引起较多内存的长期占用。
function outer() { 
    var x = HUGE; // 超大对象
    function inner() { 
      var y = GIANT; // 大对象
      use(x); // x 需要使用,需要成为Context变量
      function innerF() { 
        use(y); // y 需要使用,需要成为Context变量
      } 
      function innerG() { 
        /* 空函数体 */
      } 
      return innerG; 
    } 
    return inner();
}
var o = outer(); // HUGE and GIANT 均得不到释放

变量 o 持有的是innerG闭包,innerG持有着inner的Context,且内部闭包的Context会持有外部闭包的Context,产生Context链

上下文链
为了减轻GC压力,建议避免过深嵌套函数/闭包,或及早手动断开Context变量所引用的大对象。

2.3 大内存使用

参考资料:

**
本人技术有限,且技术更新很快,如果文中存在错误或者不足,欢迎大家指正,相互交流。
邮箱:hjaurum@gmail.com
**

上一篇下一篇

猜你喜欢

热点阅读