JavaScript的垃圾回收机制是引擎自动管理内存的核心能力,它会自动识别不再被使用的内存空间并释放,避免开发者手动操作内存带来的错误。不过如果代码编写不当,还是会出现内存无法被回收的泄漏问题。

JavaScript垃圾回收机制的核心原理
目前主流的JavaScript引擎(如V8、SpiderMonkey)主要采用两种垃圾回收策略,分别是标记清除和引用计数,其中标记清除是更通用的实现方式。
1. 标记清除(Mark-and-Sweep)
这是大多数现代JavaScript引擎默认采用的回收方式,核心流程分为两个阶段:
- 标记阶段:从根对象(如全局对象、当前执行栈中的变量)出发,遍历所有能访问到的对象,给这些对象打上可达标记。
- 清除阶段:遍历堆中所有对象,把没有可达标记的对象视为垃圾,释放其占用的内存空间。
这种方式的优势是可以解决循环引用带来的回收问题,只要对象不可达就会被回收。
2. 引用计数(Reference Counting)
引用计数的逻辑是为每个对象维护一个引用计数器,当有一个变量引用该对象时计数器加1,当引用失效时计数器减1,当计数器为0时就把对象视为垃圾回收。
这种方式的缺陷是没法处理循环引用的问题,比如两个对象互相引用,即使它们已经不再被其他变量使用,计数器也不会归零,就会导致内存泄漏。
常见的内存泄漏场景及规避方法
1. 意外的全局变量
在非严格模式下,未声明的变量会被自动挂载到全局对象上,只要页面不关闭,这些变量就不会被回收。
比如下面的代码就会产生意外的全局变量:
// 非严格模式下,未声明的变量会成为全局变量
function test() {
leakVar = "我是意外的全局变量"; // 相当于 window.leakVar = "我是意外的全局变量"
}
test();
规避方法:开启严格模式,在代码开头添加use strict,这样未声明的变量赋值会直接报错,避免意外创建全局变量。
2. 被遗忘的定时器或回调函数
如果定时器(setInterval、setTimeout)没有被清除,或者回调函数一直被引用,那么定时器内部的变量和依赖的对象都无法被回收。
示例代码如下:
let data = { name: "测试数据" };
// 定时器没有清除,即使data不再被使用,也会一直被定时器引用无法回收
setInterval(() => {
console.log(data.name);
}, 1000);
// 正确的做法是在不需要的时候清除定时器
const timer = setInterval(() => {}, 1000);
clearInterval(timer);
3. 脱离DOM的引用
如果把DOM元素存储到JS变量中,之后把这个DOM从页面移除,但是变量没有清空,那么这个DOM元素的内存也无法被回收。
// 存储DOM元素引用
const dom = document.getElementById("test");
// 从页面移除DOM
document.body.removeChild(dom);
// 此时dom变量仍然引用着这个DOM元素,导致内存无法回收
// 正确做法是在移除后清空引用:dom = null;
4. 闭包使用不当
闭包会保留其词法作用域的引用,如果闭包一直被外部引用,那么闭包内部的变量也不会被回收。
function createClosure() {
const bigData = new Array(1000000).fill("test"); // 大数组
return function() {
console.log(bigData.length);
};
}
// closure一直被外部引用,bigData也不会被回收
const closure = createClosure();
规避方法:在不需要闭包的时候,把闭包引用赋值为null,让相关变量可以被回收。
如何检测内存泄漏
开发者可以通过浏览器DevTools的Memory面板来检测内存问题,常用的方式有:
- 使用堆快照(Heap Snapshot)对比不同时间点的内存占用,查看是否有不该存在的对象一直保留。
- 使用分配时间线(Allocation instrumentation on timeline)查看内存分配情况,定位持续增长的未回收内存。
合理理解垃圾回收机制,规避常见的内存泄漏场景,可以有效提升前端应用的运行稳定性和性能。
JavaScript垃圾回收机制内存泄漏GC引用计数修改时间:2026-06-18 07:54:22