JavaScript 中 await 永远不会 resolve 的 Promise 会导致内存泄露吗?
前言
在 JavaScript 中,await 关键字用于等待一个 Promise 完成,它只能在异步函数(async function)内部使用。当 await 一个永远不会 resolve 的 Promise 时,它确实会阻塞异步函数的进一步执行,但不会直接导致内存泄露(memory leak)。然而,这种情况可能会间接导致问题,特别是在处理资源(如数据库连接、文件句柄、网络请求等)时。
为什么说它不会直接导致内存泄露?
内存泄露通常指的是程序在不需要某些内存时未能释放它,导致内存使用量持续增加。在 JavaScript(特别是在 V8 引擎中,Chrome 和 Node.js 的 JavaScript 引擎)中,垃圾回收器(Garbage Collector, GC)会定期清理不再被引用的对象。如果一个 Promise 永远不会 resolve,那么它自身及其依赖的对象(除非有其他引用指向它们)最终会因为没有任何引用指向它们而被垃圾回收器回收。
间接问题
尽管 await 一个永远不会 resolve 的 Promise 不会直接导致内存泄露,但它可能导致以下问题:
-
阻塞执行:异步函数将停留在 await 表达式处,无法继续执行后续代码,这可能会阻塞事件循环中的其他任务。
-
资源占用:如果 Promise 依赖于某些外部资源(如数据库连接、文件句柄、网络请求等),这些资源将不会被释放,直到 Promise 被解决或拒绝。如果 Promise 永远不解决,这些资源可能会长时间被占用,甚至可能导致资源耗尽。
-
死锁和性能问题:在复杂的应用程序中,多个异步操作可能相互依赖。如果一个操作因为等待一个永远不会 resolve 的 Promise 而阻塞,它可能会阻止其他依赖它的操作执行,从而导致死锁或性能问题。
想要知道 promise 对象有没有被回收掉,可以在控制台使用 queryObjects() :
queryObjects(Promise) 做的是就是先手动执行一次垃圾回收,然后输出当前页面内存里还存在的 promise 对象。有 0 个,证明所有的 promise 对象都已经被回收了。
为了更明确的看到回收的确发生了,我们还可以给传入 test() 的 promise 对象和 test() 返回的 promise 对象都添加上垃圾回收的回调:
可以看到,这两万个永远不会 resolve 的 promise 都被回收了,这也是符合预期的。
JS 标准应该没有制定垃圾回收的具体细节,任何的对象何时被回收,甚至完全不回收,可能都不算是违反规范,毕竟 test262 里没有相关测试。不过规范实际制定时肯定还是要考虑逻辑上不能存在内存泄漏的。
所以这些都是引擎实现的知识,只有少数引擎开发能讲清楚这些细节,我只知道一点皮毛,下面是我的推测。
想要一个对象不被回收,必须有地方引用了它,除了直接引用,还可以间接的引用,比如:
new Promise((resolve, reject) => { window.foo = resolve })
因为全局变量 foo 引用了 resolve 函数,这个函数比较特殊,在 C++ 层面其实引用了它所属的 promise 对象,所以会导致 promise 对象一直可达(reachable),也就无法被垃圾回收。
new Promise(resolve => { setTimeout(resolve, 10000) })
像这个 promise,在 10 秒后才会被垃圾回收,10 秒内全局的任务队列里有个定时器任务引用了它,定时器执行完销毁后,这个 promise 对象就变成不可达的,从而也就被回收了。
如果 resolve 和 reject 都没被引用,它就会被直接回收掉:
new Promise(() => {})
除非有其它引用,比如你示例里的 p:
async function test(p) { await p } test(new Promise(() => {}))
这个局部变量 p 的确引用了 promise 对象,那这个 promise 被回收只有一个可能,就是 p 也不在了,实际上的确是,这个 test 函数的执行上下文也被回收了,虽然它还没执行完。
实际上 V8 的 async function 在 parser 阶段是被 desugar 成 generator 的 https://docs.google.com/document/d/1K38ct2dsxG_9OfmgErvFld4MPDC4Wkr8tPuqmSWu_3Y/edit,所以 test 函数在实际执行时可能类似于:
function* test(p) { yield p console.log(p) } test(new Promise(() => {}))
生成器在 yield p 这里停住,就类似于 await p 停住,因为已经没有办法引用到生成器的 next() 方法了,引擎就知道它不可能继续执行了,从而就一连串回收掉了所有的相关对象,具体的细节我是讲不清楚的。
解决方案
-
超时机制:为 await 操作设置超时,以便在 Promise 无法在指定时间内 resolve 时采取适当的行动(如重试、记录错误或释放资源)。
-
错误处理:确保 Promise 的错误处理逻辑是健全的,以便在 Promise 被拒绝时能够适当响应。
-
资源清理:确保所有外部资源在使用完毕后都被正确释放,无论 Promise 是否 resolve。
-
监控和日志:对异步操作进行监控,并在出现问题时记录详细的日志,以便快速定位和解决问题。
总之,虽然 await 一个永远不会 resolve 的 Promise 不会直接导致内存泄露,但它可能导致其他严重的问题,因此应该避免这种情况的发生。
仅供参考!!!