JS的内存管理由引擎自动完成,核心目标是合理分配内存、高效使用内存、及时回收不再使用的内存,其中垃圾回收机制是实现内存自动释放的关键部分。JS的内存生命周期整体分为三个步骤,首先是内存分配,当声明变量、函数或者创建对象时,引擎会自动为这些数据结构分配对应的内存空间;其次是内存使用,对分配好的内存进行读写操作,比如修改变量值、调用对象方法等;最后是内存释放,当内存中的数据不再被使用时,垃圾回收机制会自动回收这部分内存,供后续新的数据使用。

常见的垃圾回收算法
1. 标记清除算法
标记清除是JS引擎中最主流的垃圾回收算法,大部分现代JS引擎都采用这种算法作为基础。它的执行过程分为两个阶段:
- 标记阶段:从根对象(比如全局对象、当前执行栈中的变量)开始,遍历所有能访问到的对象,给这些对象打上可访问的标记。
- 清除阶段:遍历堆中所有对象,把没有打上可访问标记的对象回收,释放其占用的内存空间。
这种算法的优势是可以解决循环引用的问题,只要两个对象互相引用但都无法从根对象访问到,就会被标记为不可达然后回收。下面是一个简单的示例:
// 标记清除示例
function createObjects() {
let objA = { name: 'objA' };
let objB = { name: 'objB' };
// 循环引用
objA.ref = objB;
objB.ref = objA;
// 函数执行结束后,objA和objB都无法从根对象访问到
// 标记清除算法会回收这两个对象的内存
}
createObjects();
2. 引用计数算法
引用计数是早期的垃圾回收算法,核心逻辑是为每个对象维护一个引用计数器,记录当前有多少个变量引用该对象:
- 当对象被新的变量引用时,引用计数加1。
- 当引用该对象的变量被重新赋值或者超出作用域时,引用计数减1。
- 当引用计数变为0时,说明对象不再被使用,立即回收其内存。
这种算法的缺陷是无法处理循环引用的问题,比如两个对象互相引用,它们的引用计数永远不会变为0,就会导致内存泄漏。示例如下:
// 引用计数缺陷示例
function createCycle() {
let objA = {};
let objB = {};
// 互相引用,引用计数都为1
objA.ref = objB;
objB.ref = objA;
// 函数执行结束,objA和objB超出作用域,但引用计数还是1
// 引用计数算法无法回收这两个对象,造成内存泄漏
}
createCycle();
常见的内存泄漏场景
即使有垃圾回收机制,不合理的代码写法还是可能导致内存泄漏,常见场景有以下几种:
- 意外的全局变量:未使用
var、let、const声明的变量会挂载到全局对象上,不会被垃圾回收。 - 被遗忘的定时器:设置了
setInterval但没有清除,定时器回调中引用的变量会一直被保留。 - 脱离DOM的引用:把DOM元素存在变量中,即使DOM从页面移除,变量还在引用该元素,元素内存不会被回收。
- 闭包的不合理使用:闭包会保留外部函数的作用域,如果闭包长期存在,外部函数中的变量也不会被回收。
避免内存泄漏的实用方法
为了避免内存泄漏,提升代码性能,可以遵循以下实践:
- 声明变量时尽量使用
let或const,避免意外的全局变量。 - 使用
setInterval或setTimeout时,在不需要的时候及时调用clearInterval或clearTimeout清除定时器。 - 如果不需要再使用DOM元素的引用,及时把对应的变量赋值为
null,解除引用。 - 合理使用闭包,不需要的闭包变量及时释放,避免长期持有不必要的引用。
- 注意事件监听的移除,给元素添加的事件监听在元素销毁时及时移除,避免回调中引用的变量无法被回收。
了解JS的内存管理和垃圾回收机制,能帮助开发者更清晰地理解代码的运行逻辑,写出更高效的JS代码,减少内存泄漏问题的发生。