GC(V8)
GC 即 Garbage Collection(垃圾回收)。在 JS 代码执行过程中,引擎会自动分配内存并在数据不再使用时释放内存。如果释放不及时或者无法释放,就会造成内存泄漏。
“V8 的垃圾回收核心是分代回收。它将内存分为新生代和老生代:新生代存放存活时间短的对象,用Scavenge (复制)算法,牺牲空间换时间;老生代存放存活时间长或体积大的对象,用标记-清除 (Mark-Sweep)和标记-整理 (Mark-Compact)算法。此外,为了避免 GC 阻塞主线程导致页面卡顿,V8 还引入了增量标记和并发回收等优化机制。”
V8 内存限制
V8 对内存的使用有严格的限制(64 位系统约 1.4G, 32 位约 0.7G)。 为什么要限制内存? 主要是因为 V8 最初是为浏览器设计的,单页面的内存需求不大;更重要的是垃圾回收机制的时间开销。如果内存过大(比如 1.5G),执行一次完整的垃圾回收可能会导致 JavaScript 线程暂停(Stop-The-World)超过 1 秒,这在浏览器端会造成极其严重的卡顿。
分代回收策略
V8 将堆内存分为两个区域:新生代(Young Generation) 和 老生代(Old Generation)。
1. 新生代 (Scavenge 算法)
新生代内存极小(通常 1~8MB),专门用来存放存活时间极短的对象。 它采用 Scavenge(清除)算法,核心思想是用空间换时间。
内存划分:将新生代内存均分为两半,一半叫
From空间,一半叫To空间。工作流程:
- 新分配的对象总是存放在
From空间。 - 当
From空间快满时,触发 GC:遍历From空间,找出存活的活动对象。 - 将这些活动对象复制到
To空间,并保证它们在To空间中内存是连续的(没有碎片)。 - 释放
From空间中所有非活动对象(直接清空)。 - 将
From和To空间角色互换,完成一次回收。
- 新分配的对象总是存放在
对象晋升机制: 如果一个对象经过多次 Scavenge 依然存活,或者
To空间的内存占用已经超过 25%,该对象会被直接转移到老生代中。
2. 老生代 (Mark-Sweep & Mark-Compact)
老生代存放体积较大的对象或从新生代晋升过来的对象。这里由于对象存活率高且体积大,不能再用复制算法(空间浪费大且复制成本高)。
标记-清除 (Mark-Sweep):
- 标记:从根对象(Root)开始遍历,能访问到的标记为“活动对象”。
- 清除:遍历堆内存,将未被标记的“非活动对象”直接清除。
- 缺点:会产生大量不连续的内存碎片。
标记-整理 (Mark-Compact): 为了解决内存碎片问题,V8 会在内存不足以分配大对象时触发此算法。
- 整理:将所有存活的活动对象向内存空间的一端移动。
- 清理:直接清理掉边界外的所有内存。
V8 垃圾回收的高级优化 (Orinoco)
传统的 GC 执行时会暂停所有 JavaScript 主线程逻辑(称为 Stop-The-World 全停顿),导致动画掉帧。V8 为此做了极致的优化:
- 增量标记(Incremental Marking):将一次长达几十毫秒的标记过程,拆分成许多毫秒级的小步。主线程执行一点 JS 代码,接着执行一点标记逻辑,交替进行,极大降低了最大停顿时间。
- 并发回收(Concurrent Sweeping/Marking):利用后台辅助线程,在不阻塞主线程的情况下,并行地去执行标记和清理工作。
- 懒清理(Lazy Sweeping):如果当前内存足够,V8 不会急着立即清理所有的死对象,而是按需延迟清理,直到内存确实不够用时再清理。
常见内存泄漏场景与排查
面试题:什么是内存泄漏?在前端开发中,通常有哪些情况会导致内存泄漏?
内存泄露(Memory Leak)是指:程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费。
常见泄漏场景
意外的全局变量 在非严格模式下,未声明直接赋值的变量会被挂载到
window上,或者在全局作用域中使用this。全局变量的生命周期与页面相同,只有在刷新或关闭时才会被释放。 解决:使用严格模式'use strict'。被遗忘的定时器或事件监听 如果组件被销毁,但它内部注册的
setInterval或全局的window.addEventListener没有被清理,那么回调函数内部引用的变量将永远无法被回收。 解决:在组件卸载(如 Vue 的onUnmounted,React 的useEffectcleanup)时,手动clearInterval和removeEventListener。闭包的不当使用 闭包会将其外部函数的变量保存在内存中。如果在长生命周期的对象中滥用了闭包,会导致外部作用域的内存一直无法释放。
游离的 DOM 引用 如果你用一个 JS 变量保存了 DOM 节点的引用,后来这个节点被从 DOM 树上移除了,但由于 JS 变量依然持有它的引用,这个 DOM 节点(及其子节点)就不会被垃圾回收,成为“游离的 DOM”。
let element = document.getElementById("button");
document.body.removeChild(element);
// 节点虽然在页面上消失了,但 element 变量还保留着引用,导致内存泄漏
element = null; // 必须手动置空解除引用
如何排查内存泄漏?
在面试中可以说出以下实际操作步骤:
- 打开 Chrome DevTools,切换到 Performance(性能) 面板。
- 勾选
Memory(内存)选项,点击录制。 - 在页面上执行可能导致泄漏的交互操作(比如多次进出同一个组件)。
- 停止录制,观察内存走势图(Heap 曲线)。
- 正常的 GC 曲线应该是波浪形的(上升后下降)。
- 如果曲线的最低点(Min)在不断攀升,说明每次 GC 后都有垃圾无法回收,这就是典型的内存泄漏。
- 进一步定位:切换到 Memory 面板,抓取 Heap Snapshot(堆快照),比对两次操作前后的快照,查找未被释放的
Detached DOM或异常增长的大对象。
早期回收策略:引用计数 (了解即可)
IE9 之前的旧版本 IE 采用的是引用计数算法。
- 核心:跟踪记录每个值被引用的次数。次数为 0 时释放。
- 致命缺点:循环引用。如果对象 A 和对象 B 互相引用对方,哪怕它们已经和根作用域断开了联系,它们的引用次数也永远是 1,导致内存永远无法回收。现代浏览器早已弃用该策略,全面拥抱“标记-清除”。