一、垃圾回收原理篇
1. 核心概念
垃圾回收(GC) 是自动管理内存的机制,用于识别并回收不再被程序使用的内存空间,避免内存泄漏。
关键问题:
- 哪些内存需要回收?
不再被任何活动对象引用的内存(即"不可达"内存) - 何时回收?
由引擎在空闲时间或内存不足时自动触发
2. 常见垃圾回收算法
(1) 引用计数(Reference Counting)
-
原理:记录每个对象被引用的次数,当引用数为0时回收
-
优点:即时回收,无暂停
-
缺点:无法处理循环引用(经典内存泄漏)
ini// 循环引用示例 let objA = { ref: null }; let objB = { ref: null }; objA.ref = objB; objB.ref = objA; // 两者引用数永远 >=1,无法回收
(2) 标记-清除(Mark-Sweep) 🌟(主流浏览器使用)
-
原理:
- 标记阶段:从根对象(全局变量、活动函数等)出发,标记所有可达对象
- 清除阶段:遍历堆内存,回收未被标记的对象
-
优点:解决循环引用问题
-
缺点:内存碎片化
(3) 标记-整理(Mark-Compact)
- 改进点:在标记后,将存活对象整理到连续内存空间,解决碎片化问题
- 应用场景:V8 老生代内存回收
(4) 分代回收(Generational Collection)
-
核心思想 :将内存分为 新生代(Young Generation) 和 老生代(Old Generation) ,分别采用不同策略
- 新生代 :存活时间短的对象,使用 Scavenge 算法(复制存活对象到新空间)
- 老生代 :存活时间长的对象,使用 标记-清除/整理
二、V8 引擎篇
1. V8 内存结构
css
V8 堆内存
├── 新生代 (1-8MB)
│ ├── From 空间
│ └── To 空间
└── 老生代
2. 新生代回收(Scavenge 算法)
-
过程:
- 将新生代分为 From 和 To 两个等大空间
- 新对象分配到 From 空间
- From 满时,标记存活对象并复制到 To 空间
- 清空 From 空间,交换 From 和 To 角色
-
晋升条件 :
对象经历一次 Scavenge 回收仍然存活,或 To 空间使用超过 25% → 晋升到老生代
3. 老生代回收(Mark-Sweep-Compact)
- 标记阶段:三色标记法(白→灰→黑)遍历可达对象
- 清除/整理阶段:回收未标记内存,整理碎片
4. V8 优化策略
- 增量标记(Incremental Marking) :将标记过程拆分为小任务,避免长时间阻塞主线程
- 空闲时间收集(Idle-Time GC) :利用渲染间隔执行 GC 任务
三、内存泄漏篇
1. 常见内存泄漏场景
场景1:未清理的定时器/事件监听
javascript
// 泄漏示例
function LeakComponent() {
useEffect(() => {
setInterval(() => {
console.log('Leaking...');
}, 1000);
}, []);
return <div>Leak Demo</div>;
}
场景2:闭包保留大对象
javascript
function createClosure() {
const hugeArray = new Array(1000000).fill('*');
return () => console.log('Closure'); // hugeArray 被闭包引用无法回收
}
场景3:游离的 DOM 引用
javascript
let elements = {
button: document.getElementById('myButton'), // 即使DOM被移除,引用仍在
image: document.getElementById('myImage')
};
// 移除DOM后,elements 仍保留引用 → 无法回收
document.body.removeChild(document.getElementById('myButton'));
2. 内存泄漏排查工具
-
Chrome DevTools:
- Memory 面板:Heap Snapshots 对比内存变化
- Performance 面板:记录内存分配时间线
- Performance Monitor:实时监控 JS Heap 大小
四、面试题篇
高频问题1:闭包一定会导致内存泄漏吗?
答 :
不一定。闭包导致内存泄漏的 必要条件 是闭包中引用了不再需要的大对象,且未及时解除引用。现代浏览器通过优化(如 V8 的逃逸分析)可自动回收未使用的闭包变量。
高频问题2:WeakMap 和 WeakSet 如何帮助内存管理?
答:
-
WeakMap/WeakSet 的键是 弱引用,不会阻止垃圾回收
-
应用场景:
scss// 使用 WeakMap 缓存大对象 const cache = new WeakMap(); function getHeavyData(obj) { if (!cache.has(obj)) { const data = heavyCalculation(obj); cache.set(obj, data); } return cache.get(obj); } // 当 obj 被回收时,对应的缓存数据自动释放
高频问题3:如何手动触发垃圾回收?
答:
-
浏览器环境(仅 Chrome 支持):
javascriptif (window.gc) { window.gc(); // 需启动 Chrome 时加参数 --js-flags="--expose-gc" }
-
Node.js 环境:
csharpglobal.gc(); // 启动时需加参数 --expose-gc
高频问题4:如何避免 DOM 内存泄漏?
答:
-
移除 DOM 后手动解除引用:
iniconst btn = document.getElementById('myBtn'); btn.remove(); // 移除DOM btn = null; // 解除引用
-
使用
WeakRef
(ES2021):javascriptconst domRef = new WeakRef(document.getElementById('myBtn')); // 通过 domRef.deref() 访问,DOM移除后自动解除
高频问题5:描述 V8 的增量标记算法原理
答:
-
目标 :减少 GC 造成的 主线程阻塞时间
-
原理:
- 将标记过程拆分为多个小任务
- 在每个 V8 的 增量标记步骤 中执行部分标记
- 穿插在 JavaScript 任务之间执行
-
效果:大幅减少单次 GC 停顿时间
五、代码实战:内存泄漏检测
使用 Chrome DevTools 定位泄漏
xml
<button id="leakBtn">创建泄漏</button>
<script>
let leakedObjects = [];
document.getElementById('leakBtn').addEventListener('click', () => {
// 每次点击创建 1MB 的数组并保留引用
leakedObjects.push(new Array(1024 * 1024).fill('*'));
});
</script>
运行 HTML
检测步骤:
- 打开 Chrome DevTools → Memory 面板
- 记录初始 Heap Snapshot
- 多次点击按钮创建泄漏
- 记录第二次 Heap Snapshot
- 对比两次快照,筛选
Array
对象查看增长情况
六、总结:前端内存管理最佳实践
- 避免全局变量:使用模块化或 IIFE 封装
- 及时清理资源:定时器、事件监听、第三方库实例
- 慎用闭包:确保不保留无用的大对象
- 使用弱引用 :
WeakMap
/WeakSet
/WeakRef
- 监控内存:定期使用 DevTools 检测
掌握这些知识后,你不仅能应对面试,更能写出高性能的前端应用!