想象一下你的电脑内存就像是一个巨大的酒店,而你的 JavaScript 代码就是酒店的前台和客人。
let user = {name: 'John'};),就像是一位客人来入住。前台(JavaScript 引擎)会为这位客人分配一个房间(一块内存空间)。酒店房间是有限的,如果只分配不释放,酒店很快就会住满,新的客人就无法入住(程序耗尽内存,导致速度变慢甚至崩溃)。垃圾回收机制就是酒店里自动化的保洁团队,它的任务就是不断地巡视酒店,找出那些空置的房间(不再使用的内存),并把它清理干净。
保洁团队(GC)的核心工作是判断哪个房间是“空置”的。它们主要使用两种策略:
📖 思路:每个值都有一个计数器,记录有多少个变量引用(指向)它。如果一个值的引用计数变为 0,则表示没有任何方式可以访问到它了,它就是垃圾。 🤩 过程:
❗致命缺陷:循环引用
由于这个无法解决的缺陷,现代浏览器引擎不再单独使用引用计数算法。
这是当前所有现代 JavaScript 引擎(V8, JavaScriptCore 等)采用的核心算法。它的概念更聪明:判断“是否可达”(Reachability),而不是数引用次数。
🤔 思路:有一组被称为根的固有可达值。例如:
从这些根出发,凡是能通过引用链(对象的属性引用等)访问到的值,都被认为是可达的(Reachable),是“正在使用的”,必须保留。反之,任何无法从根访问到的值,都被标记为不可达的(Unreachable),即是“垃圾”。
🤖 过程(两个阶段):
Mark):垃圾回收器从根对象开始,遍历整个对象图,将所有可达的值都打上一个“标记”。Sweep):垃圾回收器将所有没有被标记的内存块(即不可达的垃圾)回收并返还给操作系统。✌🏻 解决循环引用:在上面的 objA 和 objB 例子中,函数执行完毕后,objA 和 objB 这两个局部变量(根)已经消失。这两个对象无法从任何根访问到,因此它们都会被标记为不可达,并在清除阶段被回收。问题完美解决!
标记-清除算法很好,但如果每次都要遍历整个内存,会造成明显的程序暂停。为了优化用户体验,现代JS引擎在标记-清除的基础上增加了复杂的优化策略。
1️⃣ 分代回收
观察发现:绝大多数对象都是“朝生夕死(比如函数内的临时变量)。存活时间长的对象,大概率还会继续存活很久。V8 引擎将堆内存分为两个代:
Scavenge 的算法(一种复制算法)。这样做的好处:将大部分频繁但快速的回收集中在小的新生代区域,而将对性能影响大的完整回收集中在大的老生代,减少了整体的停顿时间。
2️⃣ 增量回收
GC 停顿过长,页面卡顿。GC 不再尝试一次做完所有工作,而是将完整的标记工作分解成多个小部分,穿插在 JavaScript 的执行过程中。这样虽然总时间可能更长,但每次停顿的时间很短,用户几乎无感。3️⃣ 闲时回收
GC 会尝试在 CPU 空闲时(比如在动画的每帧之间的间隔期)运行,进一步减少对关键任务的影响。1️⃣ DOM 引用管理
移除 DOM 元素时同步清理事件监听器及变量引用
2️⃣ 定时器与闭包
3️⃣ 弱引用数据结构
WeakMap 存储对象关联数据,键对象回收时值自动释放