Node.js SetTimeout 引起的内存泄露问题

开发 前端
clearTimeout 中支持传入 id 删除定时器,而之前只支持传入定时器对象。一切看起来没问题,但是实现这个特性的时候,忘了一种场景,那就是如果用户没有执行 clearTimeout,而是定时器正常触发,因为在定时器正常触发的逻辑中没有删除映射关系,从而导致了内存泄露。

这是之前写的一篇文章,分享一下避免大家踩坑。

定时器回调通常会通过闭包持有外部的对象,比如下面的例子。

function demo() {
 const dummy = {}
 setTimeout(() => {
      dummy;
   }, 10000)
}

demo();

demo 执行完后,demo 函数里的 dummy 对象是不会释放的,因为它还被 setTimeout 引用着,如果执行很多次 demo 的话,就会导致大量的内存无法被释放,直到执行完 setTimeout,这通常不是什么问题,除非 dummy 对象非常大。

但是如果是 setInterval 的话,情况就不一样了。

function demo() {
 const dummy = {}
 setInterval(() => {
      dummy;
   }, 10000)
}

demo();

上面的代码会导致 dummy 永远不会被释放,当然这个例子很直接,大家并不会写出这样的代码,但是有时候代码复杂的时候,就不好说了,比如之前帮助业务排查问题的时候经常发现 setInterval 导致的内存泄露问题,使用场景基本如下。

class Demo { 
    timer = null
    start() {
        this.timer = setInterval(() => {
            this;
        }, 10000)
    }
    stop() {
    // 通常会漏了这一句
    // clearInterval(this.timer);
    }
}

const demo = new Demo();
demo.start();
demo.stop();

所以使用 setInterval 的时候需要特别注意。

setInterval 导致内存泄露很好理解,但是 setTimeout 导致的内存泄露并不常见,因为 setTimeout 执行完后,相应的内存都会被释放了。下面分享一个因为 Node.js Core 导致的 setTimeout 内存泄露问题,相关 issue 可以参考这里。复现代码如下。

for (i = 0; i < 500000; i++) {
   +setTimeout(() => {}, 0);
}

上面的代码会导致 setTimeout 创建的 timer 对象无法释放,乍一看,我们可以会被吓到,这不就是我们平时的用法吗?但是不用担心,下面的例子并不会出现这个问题。

for (i = 0; i < 500000; i++) {
 setTimeout(() => {}, 0);
}

仔细一看,有问题的例子中 setTimeout 还有个 + 号,那么这个是做什么的呢?

这个还要说起 setTimeout 在浏览器的实现,在浏览器中,setTimeout 返回的是一个 id,但是 Node.js 中返回的是一个对象,为了和浏览器兼容,Node.js 支持把返回的对象转成 id,这个 id 是定时器对应的 async_hooks id,那么这个是怎么实现的呢?下面看一个例子。

const dummy = {
  [Symbol.toPrimitive]() {
   return 1
  }
};
console.log(+dummy)

上面例子会输出 1,可以看到通过 Symbol.toPrimitive 可以定义对象转成原生类型时的行为。下面是另一个例子。

const dummy = {
  [Symbol.toPrimitive]() {
    return "hello ";
  }
};
console.log(dummy + "world")

Node.js 正是利用这个能力实现了和浏览器的兼容,源码如下。

Timeout.prototype[SymbolToPrimitive] = function() {
  const id = this[async_id_symbol];
  if (!this[kHasPrimitive]) {
    this[kHasPrimitive] = true;
    knownTimersById[id] = this;
  }
  return id;
};

所以一开始那个例子中 +setTimeout 最终会执行上面的代码,从而拿到一个 id。但是事情没有那么简单,从上面的代码中可以看到,除了返回一个 id 外,还有另外一个逻辑,那就是把定时器对象保存到了一个 map 中,其中 key 正是给用户返回的 id,那么这个有什么用呢?看一下 clearTimeout 代码。

function clearTimeout(timer) {
  if (typeof timer === 'number' || typeof timer === 'string') {
    const timerInstance = knownTimersById[timer];
    if (timerInstance !== undefined) {
      timerInstance._onTimeout = null;
      /*
         if (timerInstance[kHasPrimitive])
            delete knownTimersById[timerInstance[async_id_symbol]];
      */
      unenroll(timerInstance);
    }
  }
}

可以看到 clearTimeout 中支持传入 id 删除定时器,而之前只支持传入定时器对象。一切看起来没问题,但是实现这个特性的时候,忘了一种场景,那就是如果用户没有执行 clearTimeout,而是定时器正常触发,因为在定时器正常触发的逻辑中没有删除映射关系,从而导致了内存泄露。具体修复方案就是删除这个映射关系就行,具体可以参考这个 PR

1. issue: https://github.com/nodejs/node/issues/53335.

2: pr: https://github.com/nodejs/node/pull/53337

责任编辑:姜华 来源: 编程杂技
相关推荐

2023-06-30 23:25:46

HTTP模块内存

2013-08-07 10:07:07

Handler内存泄露

2017-03-20 13:43:51

Node.js内存泄漏

2017-03-19 16:40:28

漏洞Node.js内存泄漏

2020-01-03 16:04:10

Node.js内存泄漏

2022-01-02 06:55:08

Node.js ObjectWrapAddon

2022-06-23 06:34:56

Node.js子线程

2015-03-10 10:59:18

Node.js开发指南基础介绍

2013-11-01 09:34:56

Node.js技术

2015-01-14 13:50:58

AndroidHandler内存泄露

2021-12-25 22:29:57

Node.js 微任务处理事件循环

2012-04-11 13:46:33

ibmdw

2020-05-29 15:33:28

Node.js框架JavaScript

2012-02-03 09:25:39

Node.js

2011-09-08 13:46:14

node.js

2011-11-01 10:30:36

Node.js

2011-09-09 14:23:13

Node.js

2011-09-02 14:47:48

Node

2012-10-24 14:56:30

IBMdw

2011-11-10 08:55:00

Node.js
点赞
收藏

51CTO技术栈公众号