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 不会直接导致内存泄露,但它可能导致其他严重的问题,因此应该避免这种情况的发生。

仅供参考!!!

相关推荐
Tigshop开源商城3 小时前
『物流设置+SEO优化』Tigshop开源商城系统 JAVA v5.8.26 版本更新!
java·开源商城系统·tigshop
坚果派·白晓明5 小时前
【鸿蒙PC三方库移植适配框架解读系列】第八篇:扩展lycium框架使其满足rust三方库适配
c语言·开发语言·华为·rust·harmonyos·鸿蒙
花间相见5 小时前
【PaddleOCR教程01】PP-OCRv5 全面指南:从模型架构到实战部署
开发语言·r语言
小短腿的代码世界5 小时前
Qt 股票订单撮合引擎:高频交易系统的核心心脏
开发语言·数据库·qt·系统架构·交互
Tigshop开源商城5 小时前
『订单税率+收货地址校验国家字段』功能上新|跨境运营更高效,Tigshop开源商城系统 JAVA v5.8.23 版本更新
java·开源商城系统·tigshop
REDcker5 小时前
C++变量存储与ELF段布局详解 从const全局到rodata与nm_readelf验证实践
java·c++·面试
kobesdu7 小时前
【ROS2实战笔记-19】ROS2 生命周期节点的启动顺序、状态转换陷阱与热备方案
java·前端·笔记·机器人·ros·ros2
谙弆悕博士7 小时前
快速学C语言——第16章:预处理
c语言·开发语言·chrome·笔记·创业创新·预处理·业界资讯
neo_Ggx237 小时前
Maven 版本管理详解:SNAPSHOT、Release 与 Nexus 仓库的区别和影响
java·maven
matlabgoodboy7 小时前
软件开发定制小程序APP帮代做java代码代编写C语言设计python编程
java·c语言·小程序