【8.18】nodejs 原理学习 - 内存控制(5) - 内存泄漏的原因和解决方式

437 阅读4分钟

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

这是内存控制章节的第五篇文章,内存泄漏

前面四篇文章讲到,

由于 V8 垃圾回收机制的限制,nodejs 在使用内存时有一些限制,只能使用部分内存(64位系统下约为1.4 GB,32位系统下约为0.7 GB)

浅析了一个 V8 垃圾回收机制中,对于老生代和新生代的回收算法。

为了让 V8 垃圾回收机制更好的工作,我们需要注意全局变量和闭包的使用,因为它们会导致内存无法立即回收,让老生代中对象增多。

我们可以通过 os 下的 totalmem 和 freemem 查看系统的内存使用情况,通过 process.memoryUsage() 查看进程的内存使用情况,nodejs 进程内存中,有V8申请的堆内存和 Buffer 使用的内存部分,Buffer 使用的内存不受堆内存大小限制

这篇文章主要讲内存泄漏的原因和解决方式:

在 V8 的垃圾回收机制下,很少会出现内存泄漏的问题。但是 nodejs 对内存泄漏十分敏感,一旦出现内存泄漏,垃圾回收时要耗费更多的时间进行扫描,会使应用响应缓慢,直到进程内存溢出,应用崩溃。

内存泄漏的原因很多,但是本质是应该被回收的对象出了意外没有回收,变成了常驻老生代的对象。

造成内存泄漏的原因可能有如下几个,我们分别看一下:

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

谨慎把内存当作缓存

缓存限制策略

如果想通过 JavaScript 的对象缓存一些结果,再下次访问的时候优先判断缓存中的内容,如果没有再去获取。这种写法是可以的,但是要谨慎,防止内存泄漏的问题。

为什么这种写法可能产生内存泄漏的问题呢?

因为一旦这个对象被当作缓存使用,它会常驻在老生代中,而对象的键越多,占用的内存空间越大,垃圾回收时会对这些对象做无用的处理,且会影响内存用量。

如果要使用这种方式的话,需要对缓存进行限制,比如把最久未使用的缓存淘汰。可以使用Isaac Z. Schlueter采用LRU算法的缓存,地址为 github.com/isaacs/node… ,合理的使用缓存。

另外在使用模块时,因为模块都会被编译执行,然后缓存到老生代。所以在设计模块时,要小心内存泄漏的出现

比如这个例子,leakArray 随着 leak 的执行不断增加,导致局部变量 leakArray 不断增加内存占用,不被释放

var leakArray = []; 
exports.leak = function () {
    leakArray.push("leak" + Math.random()); 
};

这种情况需要添加清空队列的接口,供调用者释放内存。

缓存的解决方案

使用缓存可以尽量使用进程外的缓存,外部的缓存软件有良好的过期策略和自有的内存管理

外部缓存主要使用 Redis 和 Memcatched

他们主要的好处是:

  1. 将缓存转移到外部,减少常驻内存进程的数量,让垃圾回收更高效
  2. 进程之间可以共享缓存

关注队列状态

在生产者-消费者模型中,我们会使用队列(数组对象)作为中间产物,这种情况下,一旦消费的速度小于生产的速度,就会形成堆积。

比如我们在收集日志时,如果缺少考虑,可能会使用数据库来存储日志。日志通常是海量的,数据库是构建在文件系统之上的,数据库的写入速度远小于文件系统,于是会形成数据库写入操作的堆积。内存占用增加,造成内存泄漏。

这种场景,表层的解决方式是,我们可以把日志写入到文件中,但是生产速度如果因为某些原因激增,还是会有问题的。

深度的解决方式是监控队列的长度,如果有问题应该产生报警。另一个是任何一个异步调用都应该有超时机制,调用加入队列时就开始计时,一旦没有在规定时间内完成响应,应该通过回调函数传递超时异常。或者可以采用拒绝模式,当队列拥塞时,新到来的调用直接响应拥塞错误。

以上为内存泄漏的原因和解决方式,欢迎点赞和评论~