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

新生代怎么快速清理短期对象?用 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,它们常隐式持有大量内存
obj = null,可能比加十行业务逻辑更能防止某次线上 OOM。











