实际场景中,经常可能出现既不resolve
又不reject
的Promise
对象。
例如:被取消的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后这部分内存会被自动释放。
结论
- 未执行完成的Promise(包括内部等待的回调未完成)会占用内存。
- 执行完成的Promise(包括内部等待的回调也执行完成),不占用内存,可被GC释放。
- 执行完成的Promise,即便未触发resolve或reject,也可以被GC自动释放掉。
- 综上,无需担心既不resolve也不reject的Promise对象会引发内存泄漏。
(正文完)