JavaScript 垃圾回收怎么工作?标记清除为什么能处理循环引用?
以下对话为教学模拟,不是真实面经。
🧑💻 面试官:JavaScript 垃圾回收怎样工作?
🙋♂️ 我:对象没人引用了,就可以回收。
🧑💻 面试官:两个对象互相引用,但是程序已经拿不到它们呢?
🙋♂️ 我:如果只看引用数量,可能回收不了。
🧑💻 面试官:那为什么一个忘记删除的事件监听器,又可能让页面里的对象一直留下来?
垃圾回收要判断的是“还能不能从程序的根访问到”,不是对象之间有没有引用。
面试速答(60 秒版)
JavaScript 会自动管理内存。现代垃圾回收的核心判断通常是对象的可达性:从根对象出发,沿引用还能到达的对象需要保留,无法到达的对象才可能回收。
标记清除会先标记可达对象,再清理未标记对象。因此,即使两个对象互相引用,只要整个引用环已经不可达,也可以回收。
具体引擎还会采用分代、增量等优化,不能把所有实现都描述成一次简单的全量扫描。
自动回收也不等于不会泄漏。事件监听器、长期缓存和未结束的任务,如果仍然从根持有对象,垃圾回收就不能替我们判断这些业务上已经不用的数据该删除。

知识点详解:为什么循环引用可以回收,正常引用也可能造成泄漏
内存回收,要先判断对象还有没有用途
咱们假设页面创建了一个订单列表对象。只要页面状态、函数闭包或其他活跃对象仍然能够访问它,它就可能继续被使用,运行时不能随便释放。
MDN 的内存管理说明通过可达性解释了自动回收。这里的根包括引擎需要保留的全局引用、活跃执行上下文等,具体表示由引擎决定。
所以,“我觉得这个对象没有用了”和“程序无法再访问这个对象”,不是同一件事。垃圾回收主要能判断后者,无法自动理解业务需求。
标记清除为什么不怕孤立的引用环
假设对象 A 引用 B,B 又引用 A。程序最初通过变量访问 A,后来这条外部引用消失,A、B 也没有其他入口。
从根出发时,已经走不到 A 和 B,因此它们不会因为相互引用就自动被保留。这个引用环内部连得再紧,也不能替代一条来自根的路径。
早期的引用计数思路可能因为环中对象的引用数不为零而遇到困难。标记清除采用可达性判断,能够处理这种情况。
但不要反过来认为“循环引用都没问题”。如果全局缓存仍然持有 A,A、B 就都可达;不能回收的原因是这条仍在的外部引用,而不是环本身。
一个监听器,怎样把已经离开的页面留下来
假设页面组件在 window 上注册监听器,回调函数闭包中保存了组件数据。组件从页面上移除以后,监听器却没有删除。
引用关系可能是:
window → 监听器 → 回调函数 → 组件数据
只要这条链仍然存在,组件数据就可能继续可达。垃圾回收不能因为组件在视觉上消失了,就认定回调以后绝不会使用它。
类似情况还包括:全局 Map 不断保存旧对象,定时器长期运行,订阅没有取消。正确处理是明确资源的生命周期,在组件退出或任务结束时解除相应引用,而不是反复把某个局部变量设置为 null。
当然,闭包不是天然泄漏。活跃功能需要保留数据是正常行为;泄漏指业务已经不需要的数据,被不必要地持续保留。

怎样验证泄漏,而不是只看一条上涨曲线
引擎可能分批回收、保留堆容量,也可能存在短期内存峰值。因此一次内存上涨,不能直接证明泄漏。
可以重复进入和退出同一页面,等待任务稳定,再比较堆快照。查看持续增多的对象,以及它们到根的保留路径。重点回答“是谁还持有它”,而不是仅仅回答“占用了多少”。
V8 的并发标记介绍说明实际回收过程还包含性能优化。面试回答可以先讲标记清除的基本模型,再说明引擎实现更复杂,不能承诺固定回收时间或“立即释放”。
WeakMap 等弱引用结构适合特定关联数据,但不是所有缓存的替代。它不能代替业务上的容量策略,也不适合依赖可枚举内容的普通缓存需求。
面试官继续追问
赋值为 null,内存会马上释放吗?
不会保证马上释放。如果还有其他强引用,对象仍然可达;即使已经不可达,实际回收时间也由运行时决定。
手动触发回收能解决泄漏吗?
不能解决仍然可达的数据。如果监听器或缓存一直持有对象,触发回收也不会替应用解除这些引用。
DOM 被删除以后一定能回收吗?
不一定。JavaScript 变量、闭包或其他对象仍可能持有它或相关数据。需要检查引用关系,而不是只看 DOM 是否还显示。
面试速记卡
- 核心标准:从根是否仍然可达。
- 标记清除:保留可达对象,清理未标记对象。
- 循环引用:孤立且不可达的环可以回收。
- 泄漏来源:业务不用了,但监听器、缓存或任务仍持有。
- 排查方法:重复操作并看堆快照中的保留路径。
公司面试真题
这道题暂未收录可核验的公司真题来源。你可以先阅读本文解析,或浏览已收录的公司面试真题。
浏览公司面试真题 →