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 不会直接导致内存泄露,但它可能导致以下问题:

  1. 阻塞执行 :异步函数将停留在 await 表达式处,无法继续执行后续代码,这可能会阻塞事件循环中的其他任务。

  2. 资源占用:如果 Promise 依赖于某些外部资源(如数据库连接、文件句柄、网络请求等),这些资源将不会被释放,直到 Promise 被解决或拒绝。如果 Promise 永远不解决,这些资源可能会长时间被占用,甚至可能导致资源耗尽。

  3. 死锁和性能问题:在复杂的应用程序中,多个异步操作可能相互依赖。如果一个操作因为等待一个永远不会 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 不会直接导致内存泄露,但它可能导致其他严重的问题,因此应该避免这种情况的发生。

仅供参考!!!

相关推荐
写bug写bug12 分钟前
手把手教你使用JConsole
java·后端·程序员
异常君13 分钟前
Java 中 try-catch 的性能真相:全面分析与最佳实践
java·面试·代码规范
程序员清风1 小时前
阿里二面:Kafka 消费者消费消息慢(10 多分钟),会对 Kafka 有什么影响?
java·后端·面试
幼稚园的山代王1 小时前
Prompt Enginering(提示工程)先进技术
java·人工智能·ai·chatgpt·langchain·prompt
周某某~1 小时前
二.单例模式‌
java·单例模式·设计模式
摸鱼仙人~1 小时前
深入理解Java单例模式:确保类只有一个实例
java·javascript·单例模式
SteveDraw1 小时前
C++动态链接库封装,供C#/C++ 等编程语言使用——C++动态链接库概述(总)
开发语言·c++·c#·封装·动态链接库
十五年专注C++开发1 小时前
设计模式之单例模式(二): 心得体会
开发语言·c++·单例模式·设计模式
hstar95271 小时前
三十五、面向对象底层逻辑-Spring MVC中AbstractXlsxStreamingView的设计
java·后端·spring·设计模式·架构·mvc
flyair_China2 小时前
【云架构】
开发语言·php