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