内存泄漏

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

在V8的垃圾回收机制下,在通常的代码编写中,很少会出现内存泄漏的情况。但是内存泄漏通常产生于无意间,较难排查。尽管内存泄漏的情况不尽相同,但其实质只有一个,那就是应当回收的对象出现意外而没有被回收,变成了常驻在老生代中的对象。

通常,造成内存泄漏的原因有如下几个。

  1. 缓存。
  2. 队列消费不及时。
  3. 作用域未释放。

慎将内存当做缓存

缓存在应用中的作用举足轻重,可以十分有效地节省资源。因为它的访问效率要比I/O的效率高,一旦命中缓存,就可以节省一次I/O的时间。

但是在Node中,缓存并非物美价廉。一旦一个对象被当做缓存来使用,那就意味着它将会常驻在老生代中。缓存中存储的键越多,长期存活的对象也就越多,这将导致垃圾回收在进行扫描和整理时,对这些对象做无用功。

另一个问题在于,JavaScript开发者通常喜欢用对象的键值对来缓存东西,但这与严格意义上的缓存又有着区别,严格意义的缓存有着完善的过期策略,而普通对象的键值对并没有。

如下代码虽然利用JavaScript对象创建一个缓存对象,但是受垃圾回收机制的影响,只能小量使用:

1
2
3
4
5
6
7
8
9
10
11
let cache = {};
let get = function (key) {
if (cache[key]) {
return cache[key];
} else {
// get from otherwise
}
};
let set = function (key, value) {
cache[key] = value;
};

如果每次请求服务器都把用户的cookie存放在这个对象中,那么如果成千上万个用户调用这个接口,可想而知对象中的属性非常庞大,如果超出了V8的内存限制,可能无意识的导致内存泄漏了,唯一的办法就是限制对象的大小,当添加多少个属性之后就不再添加了。

缓存的解决方案直接将内存作为缓存的方案要十分慎重。除了限制缓存的大小外,另外要考虑的事情是,进程之间无法共享内存。如果在进程内使用缓存,这些缓存不可避免地有重复,对物理内存的使用是一种浪费。

如何使用大量缓存,目前比较好的解决方案是采用进程外的缓存,进程自身不存储状态。进程之间可以共享缓存。目前,市面上较好的缓存有Redis。