V8垃圾回收自动分代进行:新生代用Scavenge复制算法快速清理短期对象,存活对象晋升老生代;老生代用Mark-Sweep清除+Mark-Compact整理,配合增量标记与并发清理降低停顿;闭包、全局变量、dom引用易致内存泄漏,因GC仅基于可达性判断。

V8 的垃圾回收不是靠你手动触发的,而是自动、分代、有策略地清理不可达对象——它不等内存爆了才动,而是在对象“没人要”时就悄悄收走。
新生代怎么快速清理短期对象?用 Scavenge 复制算法
新创建的小对象(比如函数里临时生成的 {a: 1}、new date())默认进新生代。这里空间小(通常 1–8 MB),但回收极频繁,靠的是 Scavenge 算法:把内存切成 from 和 to 两个半区,只在 from 分配对象;一满就扫描存活对象,复制到 to,然后直接丢弃整个 from 区——快得像清空一个抽屉。
- 对象只要在一次
Scavenge后还活着,大概率会被晋升到老生代 - 大对象(比如 > 1MB 的数组或字符串)会跳过新生代,直接进老生代
-
to空间使用率超过 25% 时,也会提前触发晋升,避免复制失败
老生代为什么不用复制?标记-清除 + 标记-整理才是解法
老生代堆大(几十 MB 到 GB 级)、对象多且寿命长,复制成本太高。V8 改用 Mark-Sweep(标记-清除)为主:从 window、调用栈、全局变量等“根”出发,递归标记所有可达对象;未被标记的,就是垃圾,直接回收内存。但清除后容易产生碎片——这时候就会触发 Mark-Compact(标记-整理):把存活对象往堆起始端挤,腾出大片连续空闲空间。
- 标记阶段可能暂停 js 执行(Stop-The-World),但现代 V8 已用
Incremental Marking拆成小块穿插执行,单次停顿压到毫秒级 - 清除和整理可并发进行(
Concurrent Sweeping、Parallel Compaction),不卡主线程 - 你写的
setInterval没clearInterval,或事件监听器没removeEventListener,会让对象一直被“根”间接引用,逃过标记 → 内存泄漏
为什么闭包、全局变量、DOM 引用最容易导致泄漏?
垃圾回收只看“是否可达”,不看“你是不是忘了它”。一个本该销毁的函数,如果被闭包捕获并挂在全局变量上,或者它的内部对象被某个 DOM 元素的 dataset 或自定义属性偷偷持有,那它就永远活在老生代里。
function createHandler() { const hugeData = new Array(1000000).fill('leak'); return function() { console.log(hugeData.length); // 闭包引用 hugeData }; } window.handler = createHandler(); // 挂到全局 → hugeData 永远不会被回收
- 检查泄漏最直接的方式:chrome DevTools →
Memory面板 → 拍摄堆快照(Heap Snapshot),筛选Detached DOM tree或重复出现的构造函数名 -
WeakMap和WeakRef是少数能“弱持有”对象的机制,它们不阻止 GC,适合做缓存或元数据绑定 - node.js 中长期运行的服务,要特别注意数据库连接池、日志句柄、未关闭的
Readstream,它们常隐式持有大量内存
V8 的 GC 看似全自动,但它的“判断逻辑”完全基于引用图的可达性——这意味着你写的每一行引用代码,都在悄悄参与内存命运的投票。真正难的不是理解算法,而是意识到:你删掉的一行 obj = NULL,可能比加十行业务逻辑更能防止某次线上 OOM。