面试回答: JavaScript 引擎会自动管理内存。垃圾回收器从全局对象、当前调用栈等根节点出发,标记所有仍然可达的对象,再回收不可达对象。现代引擎通常还会结合分代、增量和并发回收降低停顿时间。需要注意,GC 判断的是对象是否“可达”,而不是业务上是否“还有用”;无用对象如果仍被定时器、事件监听器或全局缓存引用,依然不会被回收,这才会形成内存泄漏。
JavaScript 中的内存使用可以概括为三个阶段:
开发者通常不需要手动释放内存,但需要管理好对象之间的引用关系。
核心判断标准是可达性(Reachability)。
垃圾回收器会从一组天然可达的“根”开始查找,例如:
从根节点沿着引用关系能够访问到的对象称为可达对象,必须保留;无法从任何根节点访问到的对象称为不可达对象,可以回收。
因此,把某个变量赋值为 null 并不会“强制 GC”,它只是断开了一条引用。对象是否能被回收,取决于是否还存在其他可达路径。
引用计数的思路是记录一个对象被引用的次数,计数归零时立即回收。它容易理解,但单独使用时无法正确处理循环引用:
函数执行结束后,外部已经无法访问 left 和 right,但两个对象仍互相引用。如果只看引用次数,它们的计数都不为零;如果看可达性,它们已经无法从根节点访问,因此可以一起回收。
现代 JavaScript 引擎不会仅靠引用计数完成垃圾回收。
标记清除(Mark-and-Sweep)是理解现代 GC 的基础模型:
循环引用本身并不等于内存泄漏。只要整个循环引用结构已经与根节点断开,它仍然会被标记清除算法回收。
如果每次回收都完整扫描整个堆,主线程可能出现明显停顿。现代引擎会在可达性分析的基础上组合多种优化策略。
大多数新对象存活时间很短,因此引擎通常按对象存活时间划分内存区域:
以 V8 为例,新生代常使用复制式回收,老生代则结合标记清除和标记整理。分代的重点不是算法名称,而是避免每次都扫描全部长期存活对象。
这些策略只能降低停顿影响,无法保证 GC 完全不打断程序执行。
GC 只知道“还能不能访问”,不知道“业务上还需不需要”。
只要全局变量 cache 一直存在,其中的对象就始终可达。即使业务已经不再需要这些对象,垃圾回收器也不能擅自释放它们。
这也是垃圾回收与内存泄漏的边界:
具体的泄漏场景和排查方法见常见内存泄漏。
当对象关联数据不应该阻止对象被回收时,可以考虑 WeakMap:
WeakMap 不会因为键仍存在于映射中,就单独阻止该键对象被回收。它适合保存 DOM 元素、组件实例等对象的附加信息。
但弱引用不是通用缓存方案:其内容何时被回收不可预测,也不能依赖它完成必须执行的资源清理。定时器、事件监听器、网络连接和 Worker 仍然需要显式关闭或解绑。
不一定。现代 GC 判断的是对象是否从根节点可达,而不是对象之间是否互相引用。只有循环引用结构仍被某个根节点间接持有时,它才无法被回收。
标准 JavaScript 没有提供可靠的强制 GC API。开发者应该解除不必要的引用和外部资源,而不是依赖手动触发垃圾回收。
不代表。引擎可能保留已申请的堆空间以便复用,短时间的大对象分配也会造成峰值。真正需要关注的是:重复执行同一操作并经过 GC 后,仍有不应存活的对象持续累积。