欢迎光临
我们一直在努力

GC(垃圾回收器)学习记录

“阻止 GC” 是 JavaScript 内存管理中的一个核心概念。

简单来说:只要你的变量(Map/Set)里还“抓着”某个对象不放,垃圾回收器(GC)就不敢把这个对象删掉,哪怕这个对象在页面上已经没用了。

为了让你彻底明白,我们需要拆解一下 GC(垃圾回收) 的工作原理和 强引用 vs 弱引用 的区别。


1. 什么是 GC (Garbage Collection)?

JavaScript 引擎(如 Chrome 的 V8)有一个自动机制叫 垃圾回收器 (GC)。 它的任务很简单:定期扫描内存,把那些“没人用”的对象清理掉,释放内存空间。

GC 判断“没人用”的标准(标记 – 清除算法):

  • 如果一个对象没有任何变量引用它(即它是“不可达”的),GC 就认为它是垃圾,可以删除。
  • 如果一个对象至少被一个变量引用着(即它是“可达”的),GC 就认为它还在被使用,绝对不能删。

2. 什么是“阻止 GC”?(强引用的代价)

当你使用普通的 Map、Set、数组或对象属性来存储另一个对象时,你建立的是一个 强引用 (Strong Reference)。

这意味着: 只要 Map 还活着,Map 里面存的那个对象就永远被视为“正在被使用”。即使那个对象在业务逻辑上已经废了(比如对应的 DOM 元素被删除了,或者用户跳转页面了),GC 也不敢动它。

🚨 内存泄漏现场演示

假设你有一个全局的缓存 userCache (Map),用来存用户信息对象:

// 全局 Map,生命周期贯穿整个应用
const userCache = new Map();

function loadUser(userId) {
// 模拟从服务器获取一个巨大的用户对象
const userObj = { id: userId, data: new Array(1000000).fill('x') };

// 【关键动作】把 userObj 存入 Map
userCache.set(userId, userObj);

return userObj;
}

function deleteUser(userId) {
// 用户注销了,或者页面关闭了
// 此时,业务代码里没有任何变量再指向那个 userObj 了
// 理论上,userObj 应该被 GC 回收

// ❌ 但是!userCache 这个 Map 依然死死地抓着 userObj 不放!
// 对 GC 来说:userCache -> userObj,这条路是通的。
// 结论:userObj 不是垃圾,不能删!

// 如果你忘记写 userCache.delete(userId),内存泄漏就发生了。
// 随着用户不断登录注销,userCache 会越来越大,直到浏览器崩溃。
}

这就是“阻止 GC”: 因为 Map 的强引用特性,它强行延长了内部对象的寿命,导致本该被回收的内存无法释放。


3. WeakMap/WeakSet 是如何“不阻止 GC”的?(弱引用)

WeakMap 和 WeakSet 建立的是 弱引用 (Weak Reference)。

这意味着: WeakMap 对里面的对象只是“轻轻搭了一把手”,并没有“死死抓住”。

  • GC 的逻辑变了:当 GC 扫描时,它会发现:“咦,除了 WeakMap 里有个弱引用指向这个对象外,其他地方都没有人引用它了。”
  • GC 的决定:“既然没别人用它了,那它就是垃圾!” -> 直接回收该对象。
  • 后续:一旦对象被回收,WeakMap 里对应的这条记录也会自动消失(就像从来没存在过一样)。
✅ 修复上面的例子

// 改用 WeakMap
const userCache = new WeakMap();
// 注意:WeakMap 的键必须是对象,这里假设 userId 是个对象或者我们用 userObj 本身做键
// 为了演示,我们假设 key 是 userObj 本身,或者某个关联的 DOM 节点

function loadUser(userObj) {
const data = { info: '…' };

// 【关键动作】弱引用存入
userCache.set(userObj, data);

return userObj;
}

function deleteUser(userObj) {
// 用户注销,外部没有变量引用 userObj 了
// let user = loadUser(…);
// user = null;

// GC 扫描发现:userObj 只有 WeakMap 里的弱引用,没有强引用了。
// GC 动作:回收 userObj!
// 连锁反应:WeakMap 中对应的 entry 自动清除。

// ✅ 完美!不需要手动 delete,内存自动释放,永不泄漏。
}


4. 形象比喻

  • 普通 Map (强引用) 就像 手铐。

    • 你把犯人(对象)拷在柱子(Map)上。
    • 只要柱子还在,犯人就必须待在那,警察(GC)不能把他带走,哪怕犯人已经刑满释放了。
    • 如果你忘了主动打开手铐(.delete()),犯人就会一直被关着(内存泄漏)。
  • WeakMap (弱引用) 就像 影子。

    • 犯人(对象)走到哪,影子(WeakMap 的记录)就跟到哪。
    • 但是影子抓不住人。
    • 一旦犯人离开了(没有其他强引用了),消失了,影子也会瞬间自动消失。
    • 你不需要专门去消灭影子。

5. 总结对比

特性强引用 (Map/Set/Object)弱引用 (WeakMap/WeakSet)
对 GC 的影响 阻止 GC。只要容器在,内容就在。 不阻止 GC。如果外部没引用,内容会被 GC 杀掉。
内存安全性 低。容易因忘记删除导致泄漏。 高。自动清理,防泄漏神器。
适用场景 需要长期持有数据、配置表、字典。 临时关联数据、缓存、DOM 绑定、私有属性。
副作用 必须手动管理生命周期 (.delete)。 数据可能“莫名其妙”消失(因为被 GC 了),所以不能依赖它做持久化存储。

一句话总结: “阻止 GC”意味着强制续命。如果你希望某个对象在没人用时赶紧死掉(释放内存),就不要用普通 Map 存它,而要用 WeakMap。


实操补充:如何验证是否被 GC 了?

不要猜,用 Chrome DevTools 看真相:

  • 打开 Chrome 开发者工具 -> Memory (内存) 面板。
  • 选择 Heap snapshot (堆快照)。
  • 操作步骤:
    • 点击 Take snapshot (快照 1)。
    • 执行你的操作:进入页面 -> 离开页面(重复几次,制造疑似泄漏)。
    • 点击 Take snapshot (快照 2)。
    • 点击 Take snapshot (快照 3) (为了触发 GC,有时需要多点几次)。
  • 对比分析:
    • 在快照 2 或 3 中,选择 Comparison (对比) 模式,对比快照 1。
    • 搜索你的组件名或变量名(如 bigData)。
    • 如果看到 Delta (增量) 是正数,且 Distance (距离) 显示它被某个全局对象(如 Window, Map, Closure)引用着,那就是泄漏了。
    • 点击引用链(Retainers),你会清晰地看到是谁(哪行代码)抓着它不放。
  • 场景会被 GC 吗?原因/对策
    纯局部变量 (data, ref, computed) ✅ 会 组件卸载后,无外部引用,自动回收。
    存入全局 Map/Object ❌ 不会 除非手动 .delete(),否则强引用一直存在。建议改用 WeakMap。
    定时器/监听器未清理 ❌ 不会 闭包持有引用。务必在 onUnmounted 清理。定时器和监听器的使用会对是否GC有影响,请查看核心结论
    DOM 元素引用 ❌ 不会 如果你在 JS 里存了 document.getElementById 的结果,即使 DOM 删了,JS 变量还指着它。建议用 WeakMap 存 DOM 关联数据。
    Vue 模板语法 (@click) ✅ 会 Vue 内部管理良好,销毁时会自动清理。
    Vue 模板指令 (v-if, v-for) ✅ 会 Vue 内部管理良好,销毁时会自动清理。

    核心结论: Vue 框架本身做得很好,大部分泄漏都是开发者自己写的“副作用代码”(定时器、全局缓存、事件监听)导致的。 只要遵循 “谁创建,谁销毁” 的原则,在 unmounted / onUnmounted 中清理干净副作用,你的对象就能被正常 GC。

    对于定时器 (setInterval):如果你用箭头函数(原因:箭头函数的特性是词法作用域Lexical This。它在定义的那一刻,就永久绑定了外部环境mounted 函数的 this),即使没用到 this,它依然会持有 this(导致泄漏)。如果你用普通函数且没用到 this,则不会持有,可以被GC。 对于事件监听 (addEventListener):逻辑同上。关键在于你定义回调函数的方式(箭头函数 vs 普通函数)以及它定义的作用域。


    补充技巧1:自定义指令或 Composables

    如果你发现到处都在写 addEventListener 和 removeEventListener,可以封装一个 Composable:

    // useEventListener.js
    import { onUnmounted } from 'vue';

    export function useEventListener(target, event, handler) {
    target.addEventListener(event, handler);

    // 自动注册清理逻辑
    onUnmounted(() => {
    target.removeEventListener(event, handler);
    });
    }

    // 组件中使用
    setup() {
    const handler = () => {};
    useEventListener(window, 'resize', handler);
    // 组件卸载时自动清理,无需手动写 removeEventListener
    }

    赞(0)
    未经允许不得转载:171主机测评 » GC(垃圾回收器)学习记录
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址