// 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 看到:
参考部分实现 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 秒内存会降下来
@William17 👏鼓掌
@William17 鼓掌鼓掌
@William17 666666666666666, 瞅了一会源码,只看到链表的添加和移除。完全不知道还有不完全gc的问题。。。。。结果我也蒙蔽,你这么一说好像是对了。。。。
@William17 谢大佬
一楼的回答说的比较完善了,只是内存没有释放的原因没有提到,for 循环前没有创建 setTimeout(fn, 100) 的话,每次会新建一个 list:
而这个 TimersList 链表中会在底层的 libuv 里面映射一个真正的 Timer:
其实发展到现在,不管是 scavange 还是 marksweep/markcompacting 都是多线程的模式并行 gc,也就是这种大的 for 循环并不会阻止掉 gc 导致 OOM,这一点可以通过加上 --trace_gc 的 flag 看到:
那么 for 循环前没有创建 setTimeout(fn, 100) 导致内存释放不掉真正的原因是 clearTimeout 的操作会调用到 reuse 方法:
这里的
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 上的定时器,这样就不会溢出。