垃圾回收的概念
垃圾回收的本质就是:自动地、周期性地找出那些“不再被需要”的内存(即从根对象出发,无论如何也访问不到的对象),并将其释放,以便后续的代码可以重新使用这部分内存。
垃圾回收的算法
1. 引用计数 (Reference Counting) - “古老但已基本被淘汰”
这是早期的一种简单算法。
-
原理:
- 为每个对象维护一个**“引用计数器”**。
- 当有一个新的引用指向该对象时,计数器 +1。
- 当一个指向该对象的引用被移除或覆盖时,计数器 -1。
- 当计数器变为 0 时,说明没有任何地方引用这个对象了,它就立即被回收。
-
优点:
- 实现简单。
- 垃圾可以被立即回收,不会累积。
-
致命缺陷:无法处理“循环引用” (Circular References)
-
场景:对象 A 的一个属性引用了对象 B,同时对象 B 的一个属性也引用了对象 A。
let objA = {}; let objB = {}; objA.b = objB; objB.a = objA; -
问题:此时,objA 和 objB 的引用计数都至少为 1。即使我们后续将 objA 和 objB 的外部引用都断开(objA = null; objB = null;),它们俩的引用计数也永远不会变为 0,因为它们在互相“续命”。
-
后果:这两个对象形成了一个无法被回收的“内存孤岛”,导致内存泄漏。
-
由于这个致命缺陷,现代 JavaScript 引擎不再使用单纯的引用计数算法。
-
2. 标记-清除 (Mark-and-Sweep) - “现代 GC 的基石”
这是目前 JavaScript 中最主流的垃圾回收算法。我们上面的“公寓管理员”比喻,描述的就是这个过程。
-
原理:
-
标记阶段 (Mark Phase) :
- 垃圾回收器从一组根 (Roots) 对象开始(在浏览器中,根通常是 window 对象)。
- 它递归地遍历所有从根对象出发可以访问到 (reachable) 的对象,并给它们打上一个“存活”的标记。
-
清除阶段 (Sweep Phase) :
- 垃圾回收器遍历整个堆内存,将所有没有被打上“存活”标记的对象,都视为垃圾,并回收它们占用的内存。
-
-
优点:
- 完美解决了循环引用的问题。在上面的例子中,objA 和 objB 形成的“孤岛”与根对象 window 之间没有任何引用链,因此它们不会被标记,最终会被一起回收。
-
缺点:
- 内存碎片化 (Memory Fragmentation) :清除后,内存空间会变得不连续,就像一块奶酪上有很多孔洞。这可能会导致后续需要分配大块连续内存时找不到合适的空间。
- “Stop-the-World” :在执行标记和清除的过程中,通常需要暂停 JavaScript 应用的执行,这可能会导致页面在垃圾回收期间出现短暂的卡顿。
3. 标记-整理 (Mark-and-Compact)
这是对“标记-清除”算法的一个改进,用于解决内存碎片化问题。
-
原理:
- 标记阶段与“标记-清除”完全相同。
- 在清除阶段,它不是直接清理垃圾,而是将所有存活的对象,向内存的一端进行移动、整理,使它们紧凑地排列在一起。
- 然后,直接清理掉边界之外的所有内存。
-
优点:没有内存碎片。
-
缺点:移动对象的成本较高,比“标记-清除”更耗时。
三、V8 引擎的分代回收 (Generational Collection) - “因材施教”
现代 JS 引擎(如 Chrome 的 V8)为了极致的性能,并不会用同一种算法处理所有对象。它采用了一种分代回收的策略,这基于一个重要的观察: “大部分对象都是‘朝生夕死’的。”
V8 将堆内存分为了两个主要区域:
-
新生代 (New Generation / Young Generation)
-
存放对象:新创建的、生命周期很短的对象。
-
特点:空间小(通常只有几 MB),但垃圾回收非常频繁。
-
回收算法:Scavenge 算法(一种复制算法)
- 将新生代空间一分为二:From 空间和To 空间。
- 新对象首先被分配在 From 空间。
- 当 From 空间快满时,触发一次“小回收 (Minor GC)”。
- GC 遍历 From 空间,将其中仍然存活的对象复制到空的 To 空间。
- 复制完成后,整个 From 空间的所有内容都被视为垃圾,被一次性清空。
- 最后,From 空间和 To 空间的角色互换。
-
优点:速度极快,没有内存碎片。非常适合处理大量“短命”对象。
-
-
老生代 (Old Generation)
-
存放对象:经过多次“小回收”后仍然存活的对象(比如全局变量、闭包引用的变量),或者一些一开始就很大的对象。
-
特点:空间大,垃圾回收频率较低。
-
回收算法:标记-清除 (Mark-and-Sweep) + 标记-整理 (Mark-and-Compact)
- 当老生代空间不足时,触发一次“主回收 (Major GC)”。
- 主要使用“标记-清除”算法。
- 当发现内存碎片化问题严重时,会触发一次“标记-整理”算法来整理空间。
-
优化:为了避免长时间的“Stop-the-World”,V8 引入了增量标记 (Incremental Marking) 和并发标记 (Concurrent Marking) 等技术,让垃圾回收的过程可以和 JavaScript 应用的执行穿插进行,从而减少卡顿。
-
四、作为开发者,我需要做什么?
垃圾回收是自动的,但我们的代码风格会直接影响其效率。
-
避免意外的全局变量:
- 所有不再需要的变量,都应该确保它们的作用域是局部的。
- let 和 const 是你的好朋友。
-
警惕闭包导致的内存泄漏:
- 这是最常见的内存泄漏来源。确保在不再需要时,解除对闭包的引用(比如移除事件监听器、清除定时器)。
-
手动解除引用:
- 对于一些占用内存巨大的对象(比如一个处理完的大数组),当你可以确定它不再被需要时,可以手动将其设置为 null,这可以帮助(但不能保证)垃圾回收器更早地识别并回收它。
let largeArray = [ ... ]; // ... process largeArray largeArray = null; // 解除引用