JavaScript 是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。释放的过程称为垃圾回收。不再用到的内存,没有及时释放, 就叫做内存泄漏。
内存生命周期
不管什么程序语言,内存生命周期基本是一致的, 一般有如下生命周期:
- 内存分配 :分配所使用的内存
- 内存使用 :使用分配到的内存(即读、写内存)
- 内存回收 :使用完毕,由垃圾回收自动回收不再使用的内存
说明:全局变量一般不会回收(关闭页面回收); 一般情况下局部变量的值不用了, 会被自动回收
内存分配
JavaScript 是在在定义变量时就完成了内存分配,也就是不需要我们手动进行分配。
var number = 123; // 给数值变量分配内存
var srting = "abc"; // 给字符串分配内存
var object = {
a: 1,
b: null,
}; // 给对象及其包含的值分配内存
// 给数组及其包含的值分配内存(就像对象一样)
var arr = [1, null, "abra"];
function fn(a) {
return a + 2;
} // 给函数(可调用的对象)分配内存
// 函数表达式也能分配一个对象
someElement.addEventListener(
"click",
function () {
someElement.style.backgroundColor = "blue";
},
false,
);
//有些函数调用结果是分配对象内存
var d = new Date(); // 分配一个 Date 对象
内存使用
使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。
// 读取内存
console.log(number); // 输出 123
// 修改内存
object.a = 2; // 修改 age 属性
console.log(object.a); // 输出 2
// 使用数组
arr.push(6); // 向数组添加新元素
console.log(arr); // 输出 [1, null, "abra", 6]
内存回收
当变量不再被使用时,JavaScript 的垃圾回收机制会自动回收不再使用的内存。栈内存中的基本数据类型,可以直接通过操作系统进行处理,而堆内存中的引用数据类型的值大小不确定,因此需要JS的引擎通过垃圾回收机制进行处理。
垃圾回收机制
两种常见的浏览器 垃圾回收算法 : 引用计数法 和 标记清除法
引用计数法
IE采用的引用计数算法, 定义“ 内存不再使用 ”。这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
简单来说就是:
- 记录引用的次数,被引用 n 次,就记录次数 n
- 如果减少一个引用就减 n–
- 如果引用次数是 0 ,则释放内存
var o = {
a: {
b: 2,
},
};
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量 o
// 很显然,没有一个可以被垃圾收集
var o2 = o; // o2 变量是第二个对“这个对象”的引用
o = 1; // 现在,“这个对象”只有一个 o2 变量的引用了,“这个对象”的原始引用 o 已经没有
var oa = o2.a; // 引用“这个对象”的 a 属性
// 现在,“这个对象”有两个引用了,一个是 o2,一个是 oa
o2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
// 但是它的属性 a 的对象还在被 oa 引用,所以还不能回收
oa = null; // a 属性的那个对象现在也是零引用了
// 它可以被垃圾回收了
限制:循环引用
如果两个对象 相互引用 ,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。
function f() {
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
return "引用计数算法无法回收,内存泄漏";
}
f();
标记清除法
从 2012 年起,所有现代浏览器都使用了标记清除垃圾回收算法。
简单来说就是:
- 标记清除算法将“不再使用的对象”定义为“ 无法达到的对象 ”。
- 垃圾回收器将定期从一个叫做根(root)的对象(在JavaScript 里,根是全局对象)开始,找所有从根开始引用的对象,然后找这些对象引用的对象……
- 从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
- 所有不能获得的对象,将会被回收,释放内存
解决了循环引用。在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。
内存泄漏
内存泄漏是指程序中不再使用的内存没有被及时回收,从而导致可用内存逐渐减少,最终可能导致程序性能下降或崩溃。以下是一些常见的内存泄漏原因:
-
全局变量:
如果不小心将变量定义为全局变量,可能会导致内存泄漏。全局变量在整个应用程序生命周期内都存在,无法被垃圾回收。function createGlobalVar() { leakedVar = "I'm a global variable"; // 没有使用 var/let/const 声明 } createGlobalVar();
-
闭包:
使用闭包时,如果不当引用了外部变量,可能会导致不必要的内存占用。虽然闭包是 JavaScript 的一个强大特性,但也要注意避免过多引用。function createClosure() { let largeNumber = 100; return function() { console.log(largeNumber); }; } const closureFunction = createClosure(); // largeNumber 仍然被 closureFunction 引用
-
DOM 事件监听器:
如果事件监听器没有被移除,可能会导致内存泄漏,尤其是在动态创建和销毁 DOM 元素时。const button = document.createElement('button'); button.addEventListener('click', () => { console.log('Button clicked'); }); // 如果不手动移除监听器,button 将保持在内存中
-
定时器和回调:
使用setTimeout
或setInterval
时,如果不清除定时器,可能会导致内存泄漏。let intervalId = setInterval(() => { console.log('Interval running'); }, 1000); // 如果不调用 clearInterval(intervalId),定时器将继续存在
预防内存泄漏的方法
- 使用局部变量:尽量使用局部变量,避免不必要的全局变量。
- 手动移除事件监听器:在不再需要时,及时移除事件监听器。
- 清除定时器:在不需要时,使用
clearTimeout
或clearInterval
清除定时器。 - 避免循环引用:使用弱引用(如
WeakMap
和WeakSet
)来避免循环引用的问题。