Skip to content

JavaScript 垃圾回收机制详解,以及与 Python、Go、Java 的核心差异

更新: 6/28/2026 字数: 0 字 时长: 0 分钟

先给结论:JS 的垃圾回收靠"可达性"判断对象死活,由 V8 这类引擎自动完成,开发者无法手动触发也无法精确控制时机。它的核心算法是标记-清除,并在此基础上做了分代优化。下面先把 JS 自己讲透,再横向对比另外三门语言。

一、JavaScript 垃圾回收的核心原理

1. 判断标准:可达性(Reachability)

JS 不看"对象有没有人用过",只看"对象现在还能不能被访问到"。引擎维护一组根对象(GC Roots),包括全局对象、当前调用栈里的变量、活跃的闭包等。从根出发,顺着引用一路能走到的对象就是"可达"的,会被保留;走不到的就是垃圾,会被回收。

JS 垃圾回收可达性判断

这种思路的好处是天然能处理循环引用。两个对象互相引用,但都连不到根,照样被判定为垃圾。早期 IE 用过的引用计数法就栽在循环引用上——计数永远归不了零,内存漏掉。现代 JS 引擎早就弃用纯引用计数,改用可达性。

2. 主流算法:标记-清除及其衍生

可达性的具体落地就是标记-清除(Mark-and-Sweep):

  • 标记阶段:从 GC Roots 出发遍历整个对象图,给所有可达对象打上标记。
  • 清除阶段:扫描堆,把没标记的对象回收掉,空间归还。

纯标记-清除有个老毛病:回收后内存变得零碎,大对象申请不到连续空间。所以引擎会再加一步标记-整理(Mark-Compact),把存活对象往一端挪,腾出连续空闲区。

3. 分代回收:V8 的工程优化

V8 基于一条经验规律——大部分对象活不久,熬过一轮回收的对象往往会活很久(代际假说)——把堆分成两块,分别用不同策略:

V8 分代式垃圾回收

  • 新生代(Young Generation):空间小,存放新对象,用 Scavenge 复制算法。它把空间一分为二(From/To),回收时把存活对象复制到另一半,然后整块清空原区。速度快,代价是浪费一半空间,但新生代本来就小,划算。对象熬过一两轮还活着,就晋升到老生代。
  • 老生代(Old Generation):空间大,存放长寿对象,用标记-清除 + 标记-整理。对象多、存活率高,复制算法不划算,所以用标记类算法配合整理来控制碎片。

为了减少卡顿,V8 还引入了增量标记(把一次长标记拆成多段穿插执行)、并发/并行回收(用后台线程分担工作),尽量不让主线程长时间停下来。

4. 触发时机:开发者管不着

GC 由引擎自行决定,典型触发点包括:新生代空间分配满了、老生代到达阈值、系统空闲时机等。JS 没有标准的手动触发 API(Node 加 --expose-gc 后能调 global.gc(),但仅用于调试,不能依赖)。开发者能做的是避免制造意外可达的对象——比如及时解绑事件监听、清理不再用的全局缓存、注意闭包持有的大对象,这些才是实战中防内存泄漏的关键。

二、JS 与 Python、Go 的核心差异对比

四门语言都做自动内存管理,但底层策略和取舍差别很大。先看一张总览:

四种语言垃圾回收机制对比

维度 1:回收策略(怎么判断垃圾)

  • JS:纯可达性。标记-清除为基础,分代优化,完全不用引用计数。
  • Python:引用计数为主,分代标记-清除为辅。每个对象带一个计数器,引用归零立刻回收——这点和 JS 根本不同,Python 大多数对象是"即时死亡"的。但引用计数处理不了循环引用,所以 CPython 额外挂了一个分代的标记-清除回收器,专门扫循环垃圾。
  • Go:三色标记 + 并发回收,不分代(主流版本)。三色法(白/灰/黑)是为并发设计的:回收线程和业务线程同时跑,用写屏障保证标记不出错。Go 追求的是低延迟,不是吞吐峰值。

维度 2:停顿表现(Stop-The-World)

这是四者体感差异最大的地方。

GC 停顿表现对比

  • JS:单线程语言,GC 时主线程必须停下。所以 V8 拼命用增量、并发标记把一次停顿切碎、塞进空隙,目标是单次停顿别超过几毫秒,避免页面掉帧。但本质上仍有 STW 窗口。
  • Python:引用计数的回收是即时、分散的,不集中停顿;但那个辅助的循环回收器触发时仍会 STW。另外 CPython 有 GIL,GC 行为和全局锁纠缠在一起。
  • Go:把"低停顿"做到了极致。并发标记让大部分回收工作和业务代码并行,STW 窗口通常压在亚毫秒级,这正是它适合高并发服务端的原因。

维度 3:设计取舍(为什么这么设计)

每门语言的 GC 都是给自己的使用场景量身定的:

维度JavaScriptPythonGo
核心算法可达性 + 标记清除引用计数为主 + 分代标记清除三色标记 + 并发回收
是否分代分代(新生代/老生代)分代(三代)辅助回收循环不分代(主流版本)
循环引用天然解决需辅助回收器专门处理天然解决
回收时机引擎决定,即时性弱引用归零即时回收 + 周期扫描按堆增长比例触发
停顿特征单线程 STW,靠增量/并发切碎即时回收分散 + 循环回收 STW并发回收,STW 亚毫秒级
设计目标浏览器流畅,不卡 UI实现简单,行为可预测服务端低延迟高并发
内存开销新生代复制浪费一半空间每对象带计数器,有额外内存写屏障 + 并发有 CPU 开销

简单说:JS 为浏览器不卡而生,Python 图实现简单和回收及时,Go 押注服务端的极低延迟。三者没有谁更优,只是约束条件不同。

补充:Java 的定位

Java 的 GC 是这几门语言里最"工程化"的——它不绑定单一算法,而是提供一整套可切换的回收器(Serial、Parallel、CMS、G1、ZGC、Shenandoah)。早期 G1 走分代 + 分区路线,平衡吞吐和停顿;新一代 ZGC、Shenandoah 用并发 + 染色指针,能把停顿压到亚毫秒,对标 Go 的低延迟。Java 的哲学是把选择权交给运维:你可以按业务是要高吞吐还是低延迟,启动时挑不同回收器并调参。JS/Python 几乎没这种调优空间,Go 则刻意只给一套、少配置。

三、一句话总结

JS 的 GC 是"单线程约束下的折中艺术":用可达性 + 分代锁定垃圾,再靠增量和并发把不可避免的停顿切碎,换取浏览器里的流畅。Python 赌引用计数的简单与即时,Go 赌并发回收的低延迟,Java 干脆把选择权交给使用者。理解这些差异,本质是理解每门语言最在意的那个约束——是 UI 流畅、是实现简单、是服务延迟,还是部署可调。