JavaScript的内存管理由引擎自动完成,开发者不需要手动分配和释放内存,但了解其底层逻辑能帮助规避很多性能问题。引擎会在变量创建时自动分配内存,在变量不再被使用时通过垃圾回收机制释放内存,不过如果代码逻辑存在缺陷,还是会出现内存无法被回收的泄漏情况。

JavaScript内存管理基础流程
JavaScript的内存生命周期分为三个阶段:内存分配、内存使用、内存释放。当声明变量、创建函数、定义对象时,引擎会自动为这些内容分配对应的内存空间;在代码执行过程中,这些内存空间会被读写使用;当内容不再被引用时,垃圾回收机制会介入释放对应的内存。
需要注意的是,JavaScript中不同类型的数据内存分配方式不同,基础类型(如number、string、boolean)会存储在栈内存中,引用类型(如object、array、function)会存储在堆内存中,栈内存的释放随执行上下文出栈自动完成,堆内存的释放则依赖垃圾回收机制。
主流垃圾回收机制解析
标记清除算法
这是目前主流JavaScript引擎(如V8)默认使用的垃圾回收算法,核心逻辑分为标记和清除两个阶段。首先会从根对象(如全局对象window、当前执行上下文的变量对象)出发,遍历所有能访问到的对象,给这些对象打上标记;之后遍历堆内存中所有对象,把没有标记的对象判定为不可达对象,直接释放其占用的内存,最后清除所有标记,等待下一轮回收。
标记清除算法的优势是能解决循环引用导致的内存无法回收问题,比如两个对象互相引用,但只要它们都无法从根对象访问到,就会被判定为垃圾回收。
引用计数算法
引用计数的逻辑是为每个对象维护一个引用计数器,当有变量引用该对象时计数器加1,当引用失效时计数器减1,当计数器为0时,对象占用的内存就会被释放。这种算法实现简单,但存在明显的缺陷:无法处理循环引用的问题。
比如下面的代码场景,两个对象互相引用,即使它们已经不再被外部使用,计数器也不会归零,就会导致内存泄漏:
// 循环引用示例
function createCycle() {
let obj1 = {};
let obj2 = {};
obj1.ref = obj2;
obj2.ref = obj1;
// 函数执行结束后,obj1和obj2的引用计数都为1,无法被回收
}
createCycle();
常见内存泄漏场景与防范方法
意外的全局变量
如果在函数内部没有使用var、let、const声明变量,这个变量会自动成为全局对象的属性,只要全局对象不被销毁,这个变量占用的内存就不会被释放,造成内存泄漏。
防范方法:严格模式下运行代码,严格模式会禁止意外的全局变量创建;声明变量时始终使用let或const,避免隐式全局变量。
// 非严格模式下的问题代码
function leakGlobal() {
leakedVar = "我是意外全局变量"; // 没有声明,直接挂载到window上
}
// 防范写法,使用let声明
function safeFunc() {
let safeVar = "我是局部变量";
}
被遗忘的定时器或回调
如果设置了setInterval或者setTimeout,但是没有在不需要的时候清除,同时定时器回调中引用了外部变量,那么这些变量会一直被引用,无法被回收。另外,事件监听器如果没有移除,也会保持对回调函数的引用,导致相关内存无法释放。
防范方法:不需要定时器时及时调用clearInterval或clearTimeout清除;事件监听器在组件销毁或者不需要时调用removeEventListener移除。
// 问题代码:定时器未清除
let timer = setInterval(() => {
console.log("定时器执行");
}, 1000);
// 如果后续没有调用clearInterval(timer),定时器会一直运行
// 防范写法
let timer = setInterval(() => {
console.log("定时器执行");
}, 1000);
// 不需要时清除
clearInterval(timer);
// 事件监听器未移除的问题
const btn = document.getElementById("btn");
function handleClick() {
console.log("点击了按钮");
}
btn.addEventListener("click", handleClick);
// 防范:不需要时移除监听
btn.removeEventListener("click", handleClick);
脱离DOM的引用
如果把DOM元素存储到JavaScript对象中,之后这个DOM元素从页面中被移除,但是JavaScript对象还保留着对这个DOM的引用,那么这个DOM占用的内存也不会被释放。
防范方法:如果不需要再使用存储的DOM引用,及时将其赋值为null,切断引用关系。
// 问题代码:存储脱离DOM的引用
const domMap = {};
const div = document.getElementById("testDiv");
domMap.div = div;
// 后续div从页面移除,但是domMap还引用着它
document.body.removeChild(div);
// 防范写法:移除DOM后清除引用
const domMap = {};
const div = document.getElementById("testDiv");
domMap.div = div;
document.body.removeChild(div);
domMap.div = null; // 切断引用
闭包使用不当
闭包会保留其词法作用域的引用,如果闭包长期存在,那么闭包中引用的外部变量也不会被回收,容易造成内存泄漏。
防范方法:不需要闭包时,及时释放闭包对外部变量的引用,避免闭包长期存活。
// 问题代码:闭包长期持有外部变量
function outer() {
let bigData = new Array(1000000).fill("test"); // 大数组
return function inner() {
console.log(bigData.length);
};
}
const closureFn = outer(); // inner闭包一直持有bigData的引用
// 防范写法:不需要时释放闭包引用
function outer() {
let bigData = new Array(1000000).fill("test");
return function inner() {
console.log(bigData.length);
};
}
const closureFn = outer();
// 不需要时赋值为null,释放引用
closureFn = null;
内存泄漏排查技巧
如果应用出现卡顿、内存占用持续升高的情况,可以通过浏览器开发者工具的内存面板进行排查。可以使用堆快照功能,对比不同时间点的内存占用情况,查看哪些对象的数量异常增长;也可以使用性能面板录制一段时间的内存变化,定位内存泄漏的触发场景。日常开发中,也可以养成定期排查内存使用情况的习惯,提前发现潜在的问题。
JavaScript垃圾回收机制内存泄漏内存管理修改时间:2026-07-14 08:42:38