k8w.io
既未Resolve又未Reject的Promise对象会导致内存泄漏吗?
2019-09-21作者:k8w

实际场景中,经常可能出现既不resolve又不rejectPromise对象。
例如:被取消的HTTP请求。
我们知道,JavaScript的内存管理是基于引用计数的,出现上述情况的Promise对象时,并没有显式的方法告知Promise“你将用不到了”,如此理论上如果出现大量这样的Promise对象,将导致内存泄漏。
然而事实是否这样呢?

测试

在NodeJS 12.x环境下,我们测试一下Promise的内存占用情况。

内部无回调的Promise

我们直接看看创建10亿个既不resolve也不reject的Promise对象后,Heap内存的变化情况。

测试脚本

let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(`程序启动时占用内存: ${Math.round(used * 100) / 100} MB`);

global.gc();
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(`启动后GC占用内存: ${Math.round(used * 100) / 100} MB`);

for (let i = 0; i < 1000000000; ++i) {
    new Promise(rs => {
        if (Math.random() === NaN) {  // 构造一个不可能的条件            
            rs();   // 永远执行不到此处,仅为了引用一下rs()
        }
    }).then(() => {
        // 不可能执行到此处
        console.log('never resolved')
    })
};

used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(`Promise创建后占用内存: ${Math.round(used * 100) / 100} MB`);

global.gc();
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(`GC后占用内存 ${Math.round(used * 100) / 100} MB`);

运行结果

程序启动时占用内存: 1.99 MB
启动后GC占用内存: 1.78 MB
Promise创建后占用内存: 2.34 MB
GC后占用内存 1.77 MB

创建10亿个未被释放的Promise对象后,内存基本毫无变化。
上面的例子,由于Promise内部函数里并没有任何回调等待和异步调用,所以猜测是不是JS引擎已经做优化,自动将Promise释放了。

考虑到此,我们使用内部有回调等待的场景再来测试一次。

回调未完成的Promise

测试脚本

let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(`程序启动时占用内存: ${Math.round(used * 100) / 100} MB`);

global.gc();
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(`启动后GC占用内存: ${Math.round(used * 100) / 100} MB`);

let rand = Math.random();
let N = 0;
for (let i = 0; i < 1000000; ++i) {
    new Promise(rs => {
        setTimeout(() => {            
            if (rand === 999) {  // 构造一个不可能的条件            
                rs();   // 永远执行不到此处,仅为了引用一下rs()
            }
        }, 86400000);   // 等待24小时后再执行,肯定完成不了了
        ++N;
    }).then(() => {
        console.log('never resolved')
    })
};

setTimeout(() => {
    console.log(N);
    used = process.memoryUsage().heapUsed / 1024 / 1024;
    console.log(`Promise创建后占用内存: ${Math.round(used * 100) / 100} MB`);

    global.gc();
    used = process.memoryUsage().heapUsed / 1024 / 1024;
    console.log(`GC后占用内存 ${Math.round(used * 100) / 100} MB`);
}, 10000); // 10秒钟后就测量内存,上面24小时的回调必定无法完成

运行结果

程序启动时占用内存: 1.99 MB
启动后GC占用内存: 1.78 MB
1000000
Promise创建后占用内存: 522.05 MB
GC后占用内存 521.98 MB

可见,内部有回调的Promise,是会占用内存的。
并且当内部回调未完成时,这些内存会被持续挂起,即便GC也不会自动释放。
那么如果回调完成,但是依旧既不resolve又不reject,这些内存又会如何呢?
继续测试……

回调已完成的Promise

测试脚本

let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(`程序启动时占用内存: ${Math.round(used * 100) / 100} MB`);

global.gc();
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(`启动后GC占用内存: ${Math.round(used * 100) / 100} MB`);

let rand = Math.random();
let N = 0;
for (let i = 0; i < 1000000; ++i) {
    new Promise(rs => {
        setTimeout(() => {
            ++N;
            if (rand === 999) {  // 构造一个不可能的条件            
                rs();   // 永远执行不到此处,仅为了引用一下rs()
            }
        }, 10)   // 10毫秒后即执行,确保这里的回调肯定执行完成
    }).then(() => {
        console.log('never resolved')
    })
};

setTimeout(() => {
    console.log(N);
    used = process.memoryUsage().heapUsed / 1024 / 1024;
    console.log(`Promise创建后占用内存: ${Math.round(used * 100) / 100} MB`);

    global.gc();
    used = process.memoryUsage().heapUsed / 1024 / 1024;
    console.log(`GC后占用内存 ${Math.round(used * 100) / 100} MB`);
}, 10000);  // 上面的回调等待10毫秒,这里等待10秒,确保到这里回调肯定执行完成

运行结果

程序启动时占用内存: 1.99 MB
启动后GC占用内存: 1.78 MB
1000000
Promise创建后占用内存: 522.57 MB
GC后占用内存 1.8 MB

可见,内部只要有回调的Promise,就是会占用内存的。
但回调执行完成后,这部分内存的引用计数应该就被清零,所以GC后这部分内存会被自动释放。

结论

  1. 未执行完成的Promise(包括内部等待的回调未完成)会占用内存
  2. 执行完成的Promise(包括内部等待的回调也执行完成),不占用内存,可被GC释放
  3. 执行完成的Promise,即便未触发resolve或reject,也可以被GC自动释放掉。
  4. 综上,无需担心既不resolve也不reject的Promise对象会引发内存泄漏。
(正文完)
留言(3条)
小明 说:
赞,一直在思考这个问题来着,我发现了我确守动手去实践真理的能力。
2019-08-26 14:51 | 1楼 | 回复
董帅 说:
还是king,最skr
2019-09-29 12:28 | 2楼 | 回复
terrorblade 说:
啥都不想说抢不到票好难受,
2019-12-24 11:47 | 3楼 | 回复
发表新留言
您的大名:
必填
电子邮箱:
不公开,仅用于向你发送回复