node内存溢出 之 懵逼了
发布于 5 个月前 作者 AviorAlong 1626 次浏览 来自 问答

最近看内存溢出相关的问题,有问题及现象如下,求解 图一如下 test1.jpg 意料之中结果如下: result1.jpg

懵逼的是 图二如下: test2.jpg 结果如下: result2.jpg

问题: 图二中的settimout是怎么影响图一的运行的

6 回复

参考部分实现 https://github.com/nodejs/node/blob/1d2fd8b65bacaf4401450edc8ed529106cbcfc67/lib/timers.js 上面的 setTimeout(noop, 100) 和下面的 setTimeout(noop, 100) 都使用了 100 作为到期时间, 使用相同到期时间的 Timeout 会被放进到同一个双向循环链表

  • 如果有上面那句, 那么, 上面的 setTimeout(noop, 100), 执行的时候, 这个链表会被创建, 后面的 setTimeout(noop, 100)执行的时候, 由于这个链表已经存在, 所以后面的 Timeout 对象都会被放进这个链表, 也就是, 只有一个链表

  • 如果没有上面那句, 那么, setTimeout(noop, 100) 执行的时候会创建一个链表并把当前的 Timeout 对象放进链表, clearTimeout执行的时候会把该 Timeout 对象从链表移除, 由于链表这时候为空, 所以这个链表也会被 delete, 所以下一次执行, 又会创建一个新的链表, 也就是说, 每次迭代都会创建一个链表和 delete

当链表被 delete 之后, 虽然你调用了 gc, 不过这里我个人推测是使用增量式的GC, 所以, 那些被 delete 的链表所占用的内存并不马上被回收

你可以在后面加一句看看过一会内存使用情况, 我这边大约过 10 秒内存会降下来

setInterval(function() {
  console.log((process.memoryUsage().heapUsed/1024/1024).toFixed(2))
}, 1000)

@William17 666666666666666, 瞅了一会源码,只看到链表的添加和移除。完全不知道还有不完全gc的问题。。。。。结果我也蒙蔽,你这么一说好像是对了。。。。

一楼的回答说的比较完善了,只是内存没有释放的原因没有提到,for 循环前没有创建 setTimeout(fn, 100) 的话,每次会新建一个 list:

// lib/timers.js
function insert(item, unrefed, start) {
	//...
	const lists = unrefed === true ? unrefedLists : refedLists;
	var list = lists[msecs];
  if (list === undefined) {
    debug('no %d list was found in insert, creating a new one', msecs);
	// 这里会每次新建一个 TimersList 链表
    lists[msecs] = list = new TimersList(msecs, unrefed);
  }
  //...
}

而这个 TimersList 链表中会在底层的 libuv 里面映射一个真正的 Timer:

function TimersList(msecs, unrefed) {
  this._idleNext = this; // Create the list with the linkedlist properties to
  this._idlePrev = this; // prevent any unnecessary hidden class changes.
  this._unrefed = unrefed;
  this.msecs = msecs;
  // 这里的 TimerWrap 来自 process.binding('timer_wrap'),即 src/timer_wrap.cc
  const timer = this._timer = new TimerWrap();
  timer._list = this;

  if (unrefed === true)
    timer.unref();
  timer.start(msecs);
}

其实发展到现在,不管是 scavange 还是 marksweep/markcompacting 都是多线程的模式并行 gc,也就是这种大的 for 循环并不会阻止掉 gc 导致 OOM,这一点可以通过加上 --trace_gc 的 flag 看到:

[64569:0x102801c00]       30 ms: Scavenge 2.7 (3.8) -> 2.4 (4.8) MB, 1.1 / 0.0 ms  allocation failure 
[64569:0x102801c00]       38 ms: Scavenge 2.9 (4.8) -> 2.8 (5.8) MB, 1.3 / 0.0 ms  allocation failure 
[64569:0x102801c00]       57 ms: Scavenge 4.1 (6.3) -> 4.3 (8.8) MB, 0.7 / 0.0 ms  allocation failure 
4.872184753417969M
[64569:0x102801c00]       91 ms: Scavenge 6.2 (10.3) -> 5.3 (11.3) MB, 1.2 / 0.0 ms  allocation failure 
[64569:0x102801c00]      103 ms: Scavenge 6.8 (11.3) -> 5.9 (11.3) MB, 1.1 / 0.1 ms  allocation failure 
[64569:0x102801c00]      117 ms: Scavenge 7.3 (11.3) -> 6.7 (17.3) MB, 3.4 / 0.1 ms  allocation failure 
[64569:0x102801c00]      134 ms: Scavenge 9.8 (17.3) -> 8.4 (17.3) MB, 3.0 / 0.2 ms  allocation failure 
[64569:0x102801c00]      150 ms: Scavenge 10.6 (17.3) -> 9.5 (19.3) MB, 5.6 / 0.2 ms  allocation failure 
[64569:0x102801c00]      165 ms: Scavenge 12.2 (19.3) -> 11.0 (27.8) MB, 3.1 / 0.2 ms  allocation failure 
[64569:0x102801c00]      200 ms: Scavenge 17.3 (27.8) -> 14.5 (29.8) MB, 7.0 / 0.3 ms  allocation failure 
[64569:0x102801c00]      226 ms: Scavenge 18.7 (29.8) -> 16.6 (33.8) MB, 8.6 / 0.4 ms  allocation failure 
[64569:0x102801c00]      254 ms: Scavenge 22.1 (33.8) -> 19.6 (50.8) MB, 7.3 / 0.3 ms  allocation failure 
[64569:0x102801c00]      344 ms: Scavenge 32.3 (50.8) -> 26.5 (54.8) MB, 13.7 / 0.6 ms  allocation failure 
[64569:0x102801c00]      415 ms: Scavenge 35.2 (54.8) -> 31.0 (61.8) MB, 16.7 / 0.9 ms  allocation failure 
[64569:0x102801c00]      499 ms: Scavenge 41.9 (61.8) -> 36.9 (65.8) MB, 18.2 / 0.7 ms  allocation failure 
[64569:0x102801c00]      568 ms: Mark-sweep 45.5 (65.8) -> 44.6 (71.8) MB, 11.1 / 2.1 ms  (+ 14.6 ms in 26 steps since start of marking, biggest step 4.3 ms, walltime since start of marking 314 ms) finalize incremental marking via stack guard GC in old space requested
50.10138702392578M
[64569:0x102801c00]      606 ms: Scavenge 51.7 (71.8) -> 44.5 (76.3) MB, 10.5 / 0.6 ms  allocation failure 
[64569:0x102801c00]      666 ms: Scavenge 56.3 (76.3) -> 50.9 (80.3) MB, 13.7 / 0.7 ms  allocation failure 
[64569:0x102801c00]      718 ms: Scavenge 60.0 (80.3) -> 55.7 (86.8) MB, 14.3 / 0.7 ms  allocation failure 
[64569:0x102801c00]      773 ms: Scavenge 66.3 (86.8) -> 61.4 (91.8) MB, 13.9 / 0.7 ms  allocation failure 
[64569:0x102801c00]      824 ms: Scavenge 71.1 (91.8) -> 66.5 (97.3) MB, 13.9 / 0.7 ms  allocation failure 
[64569:0x102801c00]      879 ms: Scavenge 76.8 (97.3) -> 72.0 (102.8) MB, 14.5 / 0.7 ms  allocation failure 
[64569:0x102801c00]      935 ms: Scavenge 82.0 (102.8) -> 77.3 (108.3) MB, 12.9 / 0.8 ms  allocation failure 
87.4111099243164M
[64569:0x102801c00]     1017 ms: Scavenge 87.4 (108.3) -> 82.7 (113.8) MB, 37.5 / 1.0 ms  allocation failure 
[64569:0x102801c00]     1101 ms: Scavenge 92.7 (113.8) -> 88.0 (119.3) MB, 17.0 / 0.7 ms  allocation failure 
[64569:0x102801c00]     1211 ms: Scavenge 98.1 (119.3) -> 93.4 (125.3) MB, 19.9 / 0.7 ms  allocation failure 
[64569:0x102801c00]     1300 ms: Mark-sweep 98.6 (125.3) -> 98.3 (129.8) MB, 21.8 / 5.0 ms  (+ 45.9 ms in 134 steps since start of marking, biggest step 4.7 ms, walltime since start of marking 199 ms) finalize incremental marking via stack guard GC in old space requested
[64569:0x102801c00]     1349 ms: Scavenge 108.8 (129.8) -> 101.7 (132.8) MB, 11.8 / 0.5 ms  allocation failure 
[64569:0x102801c00]     1400 ms: Scavenge 111.6 (132.8) -> 107.0 (139.3) MB, 13.6 / 0.7 ms  allocation failure 
[64569:0x102801c00]     1451 ms: Scavenge 117.2 (139.3) -> 112.4 (145.3) MB, 13.5 / 0.7 ms  allocation failure 
[64569:0x102801c00]     1505 ms: Scavenge 122.4 (145.3) -> 117.7 (151.8) MB, 13.2 / 1.1 ms  allocation failure 
124.96233367919922M
[64569:0x102801c00]     1561 ms: Scavenge 127.8 (151.8) -> 123.1 (154.3) MB, 14.2 / 0.7 ms  allocation failure 
[64569:0x102801c00]     1612 ms: Scavenge 133.1 (154.3) -> 128.4 (160.8) MB, 12.8 / 0.7 ms  allocation failure 
[64569:0x102801c00]     1666 ms: Scavenge 138.5 (160.8) -> 133.8 (165.8) MB, 15.2 / 0.7 ms  allocation failure

那么 for 循环前没有创建 setTimeout(fn, 100) 导致内存释放不掉真正的原因是 clearTimeout 的操作会调用到 reuse 方法:

function reuse(item) {
  // timer 和构造的 list 解绑
  L.remove(item);
  const list = refedLists[item._idleTimeout];
  // if empty - reuse the watcher
  if (list !== undefined && L.isEmpty(list)) {
    debug('reuse hit');
	// 关闭底层的 libuv 上注册的定时器
    list._timer.stop();
    delete refedLists[item._idleTimeout];
    return list._timer;
  }
  return null;
}

这里的 list._timer.stop();,这个方法会调用 timer_wrap.cc 中的 uv_timer_stop,而 uv_timer_stop 只有在事件循环的下一个循环才有机会执行释放掉,也就是必须等你编写的 for 循环执行完毕后才能释放掉每次创建 list 注册到 libuv 上的 Timer 实例,而 10e7 次的大循环,显然等不到释放的时机就会因为注册了过多的 uv_timer 而 OOM 掉了。

因此总结下就是你的例子中,gc 是会在 10e7 次的大循环中间穿插执行的,因此两种写法下每次 setTimeout/clearTimeout 创建的 Timeout 实例都会被穿插 gc 掉不会影响到堆内内存大小,但是不加 setTimeout(fn, 100) 的情况下每次创建 list 而注册到 libuv 上的定时器只有等到 10e7 次的大循环执行完毕后才有机会释放掉,这样就造成了内存溢出的现象;相比下在大循环前加上了 setTimeout(fn, 100),只会注册 1 个 libuv 上的定时器,这样就不会溢出。

回到顶部