前言
在 JavaScript 开发中,我们几乎不需要手动管理内存。这背后得益于 V8 引擎强大的垃圾回收机制。本文将深入解析 V8 如何平衡“回收效率”与“页面性能”,带你搞懂新生代、老生代及增量标记等核心概念。
一、 理论基石:代际假说 (The Generational Hypothesis)
V8 的垃圾回收策略并非一刀切,而是基于一个著名的代际假说:
- 短命鬼多:大部分对象在分配内存后很快就会变得不可访问(比如函数局部变量)。
- 长生不老:经历过多次回收依然存活的对象,往往会存在很久(比如全局变量)。
基于此,V8 将堆内存划分为 新生代 (Young Generation) 和 老生代 (Old Generation) 。
二、 副垃圾回收器:新生代(Scavenge 算法)
新生代主要存放生存时间短的对象,容量通常只有 1~8M。
1. 工作原理:双区演放
它使用 Scavenge 算法,将内存一分为二:对象区域 (From-space) 和 空闲区域 (To-space) 。
- 标记与复制:当对象区域写满时,就需要执行一次垃圾清理操作,并标记存活对象,将其有序地复制到空闲区域。
- 内存整理:由于是按顺序排列复制,空闲区域在接收对象的同时自然完成了内存整理。
- 角色翻转:复制完成后,原对象区域清空,两者角色互换,循环往复。
2. 对象晋升 (Promotion)
为了防止新生代空间被撑破,如果一个对象经过 两次 垃圾回收依然存活,它会被移动到老生代区域中。
三、 主垃圾回收器:老生代(Mark-Sweep & Mark-Compact)
老生代存放生存时间长或体积巨大的对象。
1. 为什么不继续用 Scavenge?
老生代对象多、体积大,如果频繁复制会造成巨大的性能开销,且浪费一半空间。因此,它采用 标记-清除 (Mark-Sweep) 算法。
2. 回收三步曲
- 标记 (Marking) :从根元素(如 Global, DOM 等)开始递归遍历,能到达的标记为活动对象,其余为垃圾数据。
- 清除 (Sweeping) :直接回收未被标记的内存空间。
- 整理 (Compacting) :频繁清除会产生内存碎片。为了解决空间不连续问题,V8 会在清除后进行整理,让存活对象向内存一端移动。
四、 性能优化:如何解决“全停顿” (Stop-The-World)?
1. 什么是全停顿?
JS 运行在主线程,当垃圾回收执行时,JS 脚本必须暂停,直到回收完成。这种现象叫 全停顿 (Stop-The-World) 。如果内存很大,回收时间长,用户就会感到页面“卡顿”。
2. 解决方案:增量标记 (Incremental Marking)
为了降低停顿感,V8 将标记过程拆分为许多微小的子任务,让 垃圾回收标记 与 JavaScript 逻辑 交替执行。
- 类比 Fiber:这与 React 的 Fiber 架构逻辑相似,都是将大任务切片,让出主线程控制权,保证交互流畅。
- 延迟清理:除了增量标记,V8 还使用了 并行回收(多线程加速)和 并发回收(后台线程处理)来进一步压榨性能。
五、 实战建议:如何写出“ 内存友好型”代码?
- 及时解除引用:不再使用的全局变量手动设为
null。 - 避免频繁创建临时对象:在循环内尽量复用对象,减少新生代 GC 压力。
- 谨慎处理闭包:闭包容易导致本该销毁的变量被引用,从而进入老生代,造成内存泄漏。