JS 入门通关手册(31):JS 垃圾回收机制(GC):原理 + 内存泄漏全解析
本文详解 JS 垃圾回收机制(GC)的核心原理,拆解标记 - 清除、标记 - 整理两种核心算法及 V8 分代回收策略,重点分析内存泄漏的定义、高频场景(闭包滥用、全局变量、定时器等)及解决方法,搭配面试题和实战排查技巧,帮你搞懂 GC 底层逻辑,避免内存问题,应对前端面试与开发需求。
上一篇我们讲解了闭包,其中重点提到了内存泄漏—— 而内存泄漏的本质,就是垃圾回收机制(GC)没有正确回收无用的内存。
垃圾回收机制是 JS 底层自动管理内存的核心机制,新手很少关注,但它直接影响代码性能,也是面试中 “闭包延伸考点”“性能优化考点” 的核心。本文风格统一,从原理到实战,从垃圾回收算法到内存泄漏排查,帮你彻底搞懂 GC,避免因内存问题踩坑。
一、先搞懂:什么是垃圾回收(GC)?
JS 中,我们创建的变量(基本类型、引用类型)、函数、对象,都会占用内存。但内存是有限的,当这些内容不再被使用时,JS 底层会自动识别并释放它们占用的内存,这个过程就是 垃圾回收(Garbage Collection,简称 GC)。
核心特点:
- 自动执行:开发者无需手动触发,JS 引擎(V8 等)会定期自动执行垃圾回收;
- 回收目标:“垃圾” = 不再被引用、无法被访问到的变量 / 对象 / 函数;
- 核心目的:释放内存,避免内存溢出(内存占满导致程序崩溃)。
举个简单例子
javascript
运行
// 1. 声明变量,占用内存
let num = 10;
let obj = { name: "张三" };
// 2. 变量不再被引用(变成垃圾)
num = null; // 解除 num 对 10 的引用
obj = null; // 解除 obj 对对象的引用
// 3. GC 会定期扫描,发现这些无引用的内容,释放它们的内存
二、JS 垃圾回收的核心算法(V8 引擎为主)
JS 引擎(比如 Chrome 的 V8 引擎)的垃圾回收,主要依赖两种核心算法,分别对应不同类型的内存回收,相辅相成。
1. 标记 - 清除算法(最核心,应用最广)
这是 JS 垃圾回收的基础算法,适用于引用类型(对象、数组、函数等) 的回收,也是 V8 引擎最核心的回收方式,分为两个步骤:
步骤 1:标记(Mark)
JS 引擎会从 “根对象”(全局 window /global)出发,遍历所有可访问到的对象,给这些 “活跃对象”(正在被使用、能被访问到的)打上标记。
步骤 2:清除(Sweep)
遍历整个内存,清除所有没有被标记的对象(即 “垃圾”),释放它们占用的内存。
标记 - 清除算法的特点
- 优点:实现简单,能回收大部分无用对象,是 JS 垃圾回收的核心;
- 缺点:清除后会产生 “内存碎片”(零散的空闲内存块),后续创建大对象时,可能因没有连续的空闲内存而无法分配。
2. 标记 - 整理算法(优化标记 - 清除)
为了解决标记 - 清除算法的 “内存碎片” 问题,标记 - 整理算法在 “标记” 步骤后,增加了 “整理” 步骤:
步骤 1:标记(和标记 - 清除一致)
给所有活跃对象打上标记。
步骤 2:整理(Compact)
将所有活跃对象移动到内存的一端,集中排列。
步骤 3:清除(Sweep)
清除内存另一端的所有垃圾对象,释放内存。
标记 - 整理算法的特点
- 优点:解决了内存碎片问题,内存利用率更高;
- 缺点:整理过程需要移动对象,消耗一定性能,通常在内存紧张时执行。
补充:分代回收(V8 优化策略)
V8 引擎结合上面两种算法,采用 “分代回收” 策略,将内存中的对象分为两类,分别用不同频率回收,提升效率:
- 新生代(Young Generation):存活时间短的对象(如临时变量、函数内局部对象),回收频率高,采用 “复制算法”(快速回收);
- 老生代(Old Generation):存活时间长的对象(如全局对象、长期使用的实例),回收频率低,采用 “标记 - 清除 + 标记 - 整理” 算法。
三、哪些内容会被 GC 回收?(判断垃圾的标准)
GC 回收的核心是 “判断对象是否再被使用”,只要满足以下条件之一,就会被判定为垃圾,等待回收:
- 变量没有任何引用(如
let a = 1; a = null;); - 函数执行完毕后,内部的局部变量(未被闭包引用);
- 对象的所有引用都被解除(如
obj = null;); - 无法从根对象(window)遍历访问到的对象。
反例(不会被回收的情况)
javascript
运行
// 1. 被闭包引用的外层变量(不会被回收)
function outer() {
const num = 10;
function inner() {
console.log(num);
}
return inner;
}
const fn = outer(); // fn 引用 inner,inner 引用 num,num 不会被回收
// 2. 全局变量(除非手动解除引用,否则不会被回收)
let globalObj = { name: "全局对象" };
// globalObj = null; // 不手动解除,会一直占用内存
四、内存泄漏:GC 回收不了的 “垃圾”(面试必问)
什么是内存泄漏?
当某些对象已经不再被使用,但因为代码问题,导致它们依然被根对象引用,GC 无法识别为垃圾,无法回收,长期占用内存,这种情况就是 内存泄漏。
内存泄漏的危害:长期运行的程序(如单页应用 Vue/React)中,内存会不断累积,导致页面卡顿、崩溃。
常见内存泄漏场景(实战高频)
结合前面讲的闭包,以及日常开发中的常见错误,整理 5 种高频内存泄漏场景,附解决方法:
场景 1:闭包滥用导致的内存泄漏(最常见)
javascript
运行
// 泄漏场景:闭包引用 DOM 元素,未解除引用
function fn() {
const btn = document.getElementById("btn");
btn.onclick = function() {
console.log(btn.textContent);
};
}
fn();
// 问题:btn 被闭包引用,即使 btn 被移除,也无法被 GC 回收
// 解决方法:手动解除引用
function fn() {
let btn = document.getElementById("btn");
btn.onclick = function() {
console.log(btn.textContent);
btn = null; // 解除引用,让 GC 能回收 btn
};
}
场景 2:全局变量滥用(隐性泄漏)
javascript
运行
// 泄漏场景1:未声明的全局变量(自动挂载到 window)
function fn() {
// 未用 let/const 声明,变成全局变量
unDeclaredVar = "我是隐性全局变量";
}
fn();
// 问题:unDeclaredVar 挂载到 window,不手动删除,永远不会被回收
// 泄漏场景2:全局变量长期持有大对象
window.bigData = new Array(1000000).fill(1); // 占用大量内存
// 问题:bigData 是全局变量,一直被 window 引用,无法回收
// 解决方法:
// 1. 避免隐性全局变量,用 let/const 声明
// 2. 不需要时,手动解除全局变量引用:window.bigData = null;
场景 3:DOM 元素移除后,仍有引用
javascript
运行
// 泄漏场景:DOM 被移除,但 JS 中仍有变量引用它
const btn = document.getElementById("btn");
document.body.removeChild(btn);
// 问题:btn 变量依然引用该 DOM 元素,GC 无法回收
// 解决方法:解除 DOM 引用
btn = null;
场景 4:定时器未清除(高频坑点)
javascript
运行
// 泄漏场景:定时器未清除,回调函数一直被引用
const timer = setInterval(() => {
console.log("定时器一直在运行");
}, 1000);
// 问题:即使页面不需要该定时器,只要不清除,timer 就会一直引用回调,无法回收
// 解决方法:不需要时,清除定时器
clearInterval(timer);
场景 5:事件监听器未移除
javascript
运行
// 泄漏场景:事件监听器未移除,元素被移除后仍有引用
const btn = document.getElementById("btn");
function handleClick() {
console.log("点击");
}
btn.addEventListener("click", handleClick);
document.body.removeChild(btn);
// 问题:handleClick 被事件监听器引用,btn 被监听器间接引用,无法回收
// 解决方法:移除事件监听器
btn.removeEventListener("click", handleClick);
btn = null;
五、如何排查内存泄漏?(实战技巧)
日常开发中,我们可以用 Chrome 开发者工具,快速排查内存泄漏:
- 打开 Chrome 浏览器,按 F12 打开开发者工具;
- 切换到「Memory」(内存)面板;
- 点击「Take snapshot」(拍摄快照),记录当前内存状态;
- 操作页面(如切换路由、点击按钮),模拟用户行为;
- 再次拍摄快照,对比两次快照中的 “Detached DOM nodes”(分离的 DOM 节点)和 “Retained Size”(保留大小);
- 若某类对象的数量、大小持续增加,说明存在内存泄漏,定位对应的引用代码,解除引用即可。
六、高频面试题(必考)
面试题 1:什么是 JS 垃圾回收机制?它的作用是什么?
答:JS 垃圾回收机制(GC)是 JS 引擎自动执行的内存管理机制,核心是识别并释放 “不再被引用、无法被访问” 的垃圾对象的内存。作用是避免内存溢出,释放无用内存,保证程序正常运行,提升性能。
面试题 2:JS 垃圾回收的核心算法有哪些?
答:核心有两种:
- 标记 - 清除算法:先标记活跃对象,再清除未标记的垃圾对象,是基础算法;
- 标记 - 整理算法:在标记 - 清除的基础上,增加 “整理” 步骤,解决内存碎片问题。V8 引擎采用分代回收策略,结合两种算法,提升回收效率。
面试题 3:什么是内存泄漏?常见的内存泄漏场景有哪些?
答:内存泄漏是指不再被使用的对象,因依然被引用,导致 GC 无法回收,长期占用内存的现象。常见场景:1. 闭包滥用;2. 全局变量滥用;3. DOM 移除后仍有引用;4. 定时器未清除;5. 事件监听器未移除。
面试题 4:如何解决闭包导致的内存泄漏?
答:核心是及时解除闭包中的引用,让外层函数的作用域能被 GC 回收。比如:将闭包中引用的变量、DOM 元素赋值为 null,手动解除引用。
面试题 5:V8 引擎的分代回收策略是什么?
答:V8 将内存中的对象分为新生代和老生代,采用不同的回收策略:
- 新生代:存活时间短的对象,回收频率高,用复制算法,快速回收;
- 老生代:存活时间长的对象,回收频率低,用标记 - 清除 + 标记 - 整理算法,避免内存碎片。
七、总结(核心要点)
- 垃圾回收(GC):JS 引擎自动回收无用内存的机制,无需开发者手动触发;
- 核心算法:标记 - 清除(基础)、标记 - 整理(解决内存碎片);
- 分代回收:V8 引擎的优化策略,分新生代、老生代,提升回收效率;
- 内存泄漏:垃圾无法被回收的现象,核心是 “无用对象仍被引用”;
- 实战重点:避免闭包滥用、全局变量滥用,及时清除定时器、事件监听器,解除无用引用;
- 面试重点:GC 定义、核心算法、内存泄漏场景及解决方法。
掌握垃圾回收机制,不仅能避免开发中的内存泄漏问题,提升代码性能,也能轻松应对面试中 “闭包延伸”“性能优化” 相关的考点。下一篇我们将讲解 JS 错误处理与调试技巧,帮你快速定位和解决开发中的 bug。
📌 所有代码可直接复制到浏览器控制台运行,结合 Chrome 开发者工具,实操排查内存泄漏,加深理解
更多推荐
所有评论(0)