返回首页

垃圾回收机制(GC)

布莱克2026-06-01 19:31
Tip:文章封面与内容无关,作者旅游时拍摄,因为没什么值得把四季都错过!

JS引擎通过可达性判断内存是否需要保留:

从根对象(Roots)出发,能访问到的对象标记为"存活",无法访问的即为"可回收"

这里的“根”可以理解为浏览器或 Node.js 中的全局对象,比如 windowglobalThis,或者正在执行的函数上下文

标记-清除(现代引擎核心)

原理

  1. 标记阶段:从根开始遍历,标记所有可达对象
  2. 清除阶段:回收未标记的内存

优点:完美解决循环引用(互相引用但都不可达时,都不会被标记)


V8引擎的进阶策略:分代回收

V8将内存堆分为两个区域,针对性优化:

新生代(Young Generation)

存放的是:临时变量、局部变量、短时间内用完就丢弃的对象

  • 特点:存活时间短(局部变量、临时对象)
  • 空间:1-20MB,小且频繁回收
  • 算法:Scavenge(复制算法)内存分为From和To两个半区只复制存活对象到To区,清空From区速度极快(毫秒级),但浪费一半空间
  • 晋升:存活超过2次GC的对象 → 移入老生代


老生代(Old Generation)

存放的是:全局变量、闭包变量、长期存活的对象

  • 特点:存活时间长(全局变量、闭包、大型数组)
  • 空间:大且回收成本高
  • 算法:Mark-Compact(标记-整理)并发标记:后台线程标记,不阻塞主线程整理:移动存活对象,消除内存碎片清除:回收边界外内存


老生代 GC 的触发时机

触发条件1:老生代空间快满了

V8 为老生代设置了内存阈值,当老生代中的对象数量超过这个阈值,就会触发一次 Major GC(全量垃圾回收)

触发条件2:新生代晋升失败

当新生代的对象要晋升到老生代,但老生代空间不足时,也会强制触发老生代 GC。

触发条件3:内存压力信号(操作系统/浏览器)

浏览器会监听系统内存状态:

  • 操作系统内存不足时,浏览器会主动通知 V8 进行激进回收
  • 页面切换到后台时,降低 GC 阈值,更积极地回收
  • Chrome 的 Tab 休眠机制:长时间不用的 Tab 会被冻结,触发 GC


内存泄漏:

存泄漏:不再需要的内存,由于某种原因未被GC回收,导致内存占用持续增长

场景1:意外创建的全局变量

function leak() {
    // 没有 var/let/const,挂载到 window
    accidentalGlobal = new Array(1000000);
    // 函数结束,数组仍被全局引用 → 泄漏
}

// 修复:使用严格模式 'use strict' 禁止隐式全局变量

场景2:被遗忘的定时器和回调

场景3:脱离DOM的引用

let cache = {};
function addElement() {
    const div = document.getElementById('container');
    cache.div = div;          // JS 持有 DOM 引用
    div.remove();             // 从 DOM 树移除
    // 但 cache.div 还在引用,DOM 节点无法回收 ❌
}

// 修复:移除时同步清除引用
function fixedAddElement() {
    const div = document.getElementById('container');
    cache.div = div;
    div.remove();
    delete cache.div;          // 或 cache.div = null
}

场景4:闭包滥用

场景5:事件监听器未移除

场景6:Map/Set 无限增长

let userCache = new Map();
function cacheUser(id, data) {
    userCache.set(id, data);  // 只增不减
}
// 使用 WeakMap 自动回收
let userWeakCache = new WeakMap(); // 键为对象,弱引用



assistant