为何这段含Promise.race的JavaScript代码会出现内存泄漏?
为什么这段
Promise.race代码会出现内存泄漏? 问题重现
以下代码在Node.js 20.10.0中运行时,会触发JavaScript heap out of memory错误:
// ephemeralLeak.js async function promiseValue(value) { return value; } async function run() { for (;;) { await Promise.race([promiseValue("foo"), promiseValue("bar")]); } } run();
运行命令:
node --max-old-space-size=512 test/examples/ephemeralLeak.js
作为对比,使用setImmediate异步resolve的版本则无内存泄漏:
// setImmediateNoLeak.js function promiseValue(value) { return new Promise((resolve) => { setImmediate(() => resolve(value)); }); } async function run() { for (;;) { await Promise.race([promiseValue("foo"), promiseValue("bar")]); } } run();
原因分析
核心问题出在同步resolved的Promise(立即处于fulfilled状态的Promise)的V8引擎处理机制上:
- 当
promiseValue直接返回值时,async函数会将其包装为一个立即fulfilled的Promise。V8对这类Promise有特殊优化,会尽量同步处理其then回调。 - 在无限循环中,每次
await Promise.race(...)都会为两个立即fulfilled的Promise添加then订阅者。由于循环在微任务队列处理过程中立即重启,V8的垃圾回收机制无法及时清理这些临时Promise及其关联的回调链——这些对象的引用会被微任务队列的内部结构暂时持有,随着循环迭代不断累积,最终耗尽堆内存。 - 而
setImmediate版本中,Promise的resolve操作被推迟到宏任务队列执行。每次await会等待宏任务完成后再进入下一次循环,此时上一轮的Promise及其回调已被完全处理,无残留引用,因此可以被正常垃圾回收。
修复方案
除了setImmediate,更轻量的方案是让Promise的resolve操作延迟到当前事件循环的微任务阶段,避免触发V8对立即fulfilled Promise的同步优化:
// fixedVersion.js function promiseValue(value) { return Promise.resolve().then(() => value); } async function run() { for (;;) { await Promise.race([promiseValue("foo"), promiseValue("bar")]); } } run();
也可以使用Node.js原生的process.nextTick实现类似效果:
function promiseValue(value) { return new Promise(resolve => process.nextTick(() => resolve(value))); }
这种处理方式让Promise不会在创建时立即进入fulfilled状态,而是等待微任务队列执行时才resolve,确保每次循环的await能等待微任务处理完成,上一轮的Promise及其关联回调会被及时回收,不会累积内存。
内容的提问来源于stack exchange,提问作者cefn
相关产品推荐
相关产品推荐

