NodeJS中的内存泄露

2,925 阅读4分钟

  内存泄露(Memory Leak)指由于疏忽或错误造成程序未能释放已经不再使用内存的情况。如果内存泄露的位置比较关键,那么随着处理的进行可能持有越来越多的无用内存,这些无用内存的变多会引起服务器响应速度变慢,严重的情况下导致内存达到某个极限(可能是进程的上限,如V8的上限;也可能是系统可提供的内存上限)会使得应用程序奔溃。
  传统的 C/C++ 中存在指针,对象在用完之后未释放等情况导致的内存泄露。而在使用虚拟机执行的语言中如 Java、Javascript 由于使用了GC(Garbage Collection,垃圾回收)机制自动释放内存,使得程序员的精力得到了极大的解放,不用再像传统语言那样时刻对于内存的释放而战战兢兢。
  但是,即使有了GC机制可以自动释放,但这并不意味着内存泄露的问题不存在了。内存泄露依旧是开发者们不能绕过的一个问题。


GC in Node.js

  Node.js使用V8作为Javascript的执行引擎,所以讨论Node.js的GC情况就等同于在讨论V8的GC。在V8中一个对象的内存释放被释放,是看程序中是否还有地方持有该对象的引用。
  在V8中,每次GC时,是根据root对象(浏览器环境下的window,Node.js环境下的global)依次梳理对象的引用,如果能从root的引用链到达访问,V8就会将其标记为可到达对象,反之为不可到达对象。被标记为不可到达对象(即无引用的对象)后就会被V8回收。
  在NodeJS中内存泄露的原因就是:本该被清除的对象,被可到达对象引用之后,未被正确的清除而常驻内存。


内存泄露的几种情况

一、全局变量

a = 10; // 未声明对象
global.b = 11; // 全局变量引用

这种原因比较简单,全局变量直接挂在 root 对象上,不会被清除掉。


二、闭包

function out(){
    const data = Buffer.alloc(100);
    inner = function(){
        void data
    }
}

  闭包会引用到父级函数中的变量,如果闭包未释放,就会导致内存泄露。
  这里的例子只是简单的将引用挂在全局对象上,实际的业务情况可呢呢个是挂在某个可以从root追溯到的对象上导致的。


三、事件监听

  NodeJS的事件监听也可能出现内存泄露。例如对同一个事件重复监听,忘记移除(removeListener),将造成内存泄露。这种情况很容易在复用对象上添加事件时出现。
  例如,NodeJS中Agent的keepAlive为true时,可能造成内存的泄露。当Agent keepAlive为true时,将会复用之前使用过的socket,如果在socket上添加事件监听,忘记清除的话,因为socket的复用,将导致事件重复监听从而导致内存泄露
  原理上与添加事件监听忘了清除是一样的。在使用NodeJS的http模块时,不通过keepAlive复用是没有问题的,复用了以后就可能产生内存泄露。所以你需要了解添加事件监听对象的生命周期,并注意自行移除。


其他原因

  还有一些其他的情况可能会导致内存泄露,比如缓存。在使用缓存的时候,得清楚缓存的对象是多少,如果缓存对象非常多,得做限制最大缓存数量处理。还有就是非常占用CPU的代码也会导致内存泄露,服务器在运行的时候,如果有高CPU的同步代码,因为NodeJS是单线程的,所以就不能处理后面的请求了,请求堆积导致内存占用过高。


如何避免内存泄露

  • ESLint检测代码,排查非期望的全局变量。
  • 使用闭包的时候,得知道闭包了什么对象,还有引用闭包的对象何时清除闭包。最好可以避免写出复杂的闭包,因为复制的闭包引起的内存泄露,如果没有打印内存快照的话,是很难看出来的。
  • 绑定事件的时候,一定得在恰当的时候清除事件。在编写一个类的时候,推荐使用init函数对类的事件监听进行绑定和资源申请,然后destory函数对事件和占用资源进行释放。