Appearance
JavaScript 垃圾回收机制详解,以及与 Python、Go、Java 的核心差异
更新: 6/28/2026 字数: 0 字 时长: 0 分钟
先给结论:JS 的垃圾回收靠"可达性"判断对象死活,由 V8 这类引擎自动完成,开发者无法手动触发也无法精确控制时机。它的核心算法是标记-清除,并在此基础上做了分代优化。下面先把 JS 自己讲透,再横向对比另外三门语言。
一、JavaScript 垃圾回收的核心原理
1. 判断标准:可达性(Reachability)
JS 不看"对象有没有人用过",只看"对象现在还能不能被访问到"。引擎维护一组根对象(GC Roots),包括全局对象、当前调用栈里的变量、活跃的闭包等。从根出发,顺着引用一路能走到的对象就是"可达"的,会被保留;走不到的就是垃圾,会被回收。

这种思路的好处是天然能处理循环引用。两个对象互相引用,但都连不到根,照样被判定为垃圾。早期 IE 用过的引用计数法就栽在循环引用上——计数永远归不了零,内存漏掉。现代 JS 引擎早就弃用纯引用计数,改用可达性。
2. 主流算法:标记-清除及其衍生
可达性的具体落地就是标记-清除(Mark-and-Sweep):
- 标记阶段:从 GC Roots 出发遍历整个对象图,给所有可达对象打上标记。
- 清除阶段:扫描堆,把没标记的对象回收掉,空间归还。
纯标记-清除有个老毛病:回收后内存变得零碎,大对象申请不到连续空间。所以引擎会再加一步标记-整理(Mark-Compact),把存活对象往一端挪,腾出连续空闲区。
3. 分代回收: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)
这是四者体感差异最大的地方。

- JS:单线程语言,GC 时主线程必须停下。所以 V8 拼命用增量、并发标记把一次停顿切碎、塞进空隙,目标是单次停顿别超过几毫秒,避免页面掉帧。但本质上仍有 STW 窗口。
- Python:引用计数的回收是即时、分散的,不集中停顿;但那个辅助的循环回收器触发时仍会 STW。另外 CPython 有 GIL,GC 行为和全局锁纠缠在一起。
- Go:把"低停顿"做到了极致。并发标记让大部分回收工作和业务代码并行,STW 窗口通常压在亚毫秒级,这正是它适合高并发服务端的原因。
维度 3:设计取舍(为什么这么设计)
每门语言的 GC 都是给自己的使用场景量身定的:
| 维度 | JavaScript | Python | Go |
|---|---|---|---|
| 核心算法 | 可达性 + 标记清除 | 引用计数为主 + 分代标记清除 | 三色标记 + 并发回收 |
| 是否分代 | 分代(新生代/老生代) | 分代(三代)辅助回收循环 | 不分代(主流版本) |
| 循环引用 | 天然解决 | 需辅助回收器专门处理 | 天然解决 |
| 回收时机 | 引擎决定,即时性弱 | 引用归零即时回收 + 周期扫描 | 按堆增长比例触发 |
| 停顿特征 | 单线程 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 流畅、是实现简单、是服务延迟,还是部署可调。