JavaScript内存管理机制与垃圾回收算法详解
本文深入解析JavaScript的内存生命周期,涵盖内存分配、使用及释放三个阶段。重点对比了引用计数算法与标记清除算法的工作原理,详细阐述循环引用导致的内存泄漏问题及其在IE6/7中的表现,并介绍现代浏览器采用的不可达对象判定标准,帮助开发者理解自动内存管理的局限性与优化方向。
JavaScript内存管理概述
介绍
C等低级语言提供malloc()和free()等底层内存管理命令,需开发者手动释放内存。相比之下,JavaScript等高级语言在创建对象(如objects、strings等)时自动分配内存,并在对象不再使用时由系统自动回收,这一过程称为垃圾回收。垃圾回收机制的存在常使开发者误以为无需关注内存管理细节。
内存生命周期与分配机制
内存生命周期
无论何种编程语言,内存的生命周期基本一致。
1. 分配所需的内存空间
2. 对内存进行读写操作
3. 当内存不再需要时,释放所占资源
步骤1和2在所有语言中均显而易见。步骤3在低级语言中需开发者显式执行,而在JavaScript等高级语言中,此操作由解析器自动完成,因此开发者通常感知不到。
JavaScript中的内存分配操作
值的初始化过程
在JavaScript中,为变量赋值时会自动完成内存分配。
代码如下:
var n = 123; // 为数字分配内存
var s = "azerty"; // 为字符串分配内存
var o = {
a: 1,
b: null
}; // 为包含属性值的对象分配内存
var a = [1, null, "abra"]; // 为包含值的数组分配内存
function f(a){
return a + 2;
} // 为函数分配内存(函数是可调用对象)
// 函数表达式同样属于对象,也会涉及内存分配
someElement.addEventListener('click', function(){
someElement.style.backgroundColor = 'blue';
}, false);通过函数调用完成分配
部分函数在执行完毕后,也会产生对象分配的情况。
代码如下:
var d = new Date();
var e = document.createElement('div'); // 分配一个DOM元素某些方法会分配新值或新对象。
代码如下:
var s = "azerty";
var s2 = s.substr(0, 3); // s2是一个新字符串
// 由于字符串不可变,JavaScript会为[0, 3]范围内的内容创建新字符串
var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2); // 将a和a2结合,生成新数组对值的使用
使用值即是对已分配内存进行读写操作,包括读写变量或对象属性,以及向函数传递参数。
内存释放与垃圾回收基础
当内存不再需要时,释放内存
绝大多数内存管理问题发生在此阶段。最难点在于判断分配的内存是否不再需要,这通常需开发者判定程序何时不再需要内存并释放资源。
高级语言解析器内置“垃圾收集器”,负责跟踪内存分配与使用,判断内存需求并在无需时释放资源。由于判断内存是否必需是不确定问题(无法通过单一算法解决),垃圾收集器只能提供近似解。
垃圾回收算法:引用计数与标记清除
垃圾回收
如前所述,自动判定“内存不再需要”存在局限。本节将解释相关概念,介绍主要垃圾收集算法及其局限性。
引用
垃圾回收的核心概念是“引用”。在内存管理中,若一个对象显式或隐式地使用另一个对象,则称前者引用后者。例如,JavaScript对象存在指向原型的隐式引用,以及指向属性值的显式引用。
此处的“对象”概念超越JavaScript传统定义,还包括函数作用域和全局作用域。
下面要介绍的是一种最理想化的算法,引入了 “对象不再需要” 和 “没有其他对象引用该对象” 的概念。当该对象的引用指针变为0的时候,就认为他可以被回收。
以下介绍一种理想化算法,引入“对象不再需要”和“无其他对象引用该对象”的概念。当对象引用指针降为0时,即认为可回收。
例子:
var o = {
a: {
b:2
}
}; // 创建两个对象。一个对象(a)被另一个对象(o引用的对象)引用,并作为其属性
// 该对象又被变量o引用
// 显然,此时无对象可回收
var o2 = o; // 变量o2再次引用该对象
o = 1; // o不再引用该对象,仅o2仍在引用
var oa = o2.a; // oa引用o2的属性对象a
// 该对象被其他两个对象引用,分别是o2的属性a和变量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 "azerty";
}
f();
// 创建两个对象并形成相互引用
// 函数调用结束后,它们虽脱离函数作用域且不再使用,但不会被释放
// 因为引用计数算法判定只要对象存在被引用情况,便不能执行垃圾回收现实中的例子
在IE6、7中,DOM对象使用引用计数算法时存在内存泄漏问题。
代码如下:
var div = document.createElement("div");
div.onclick = function(){
doSomething();
}; // div通过click属性引用事件处理程序
// 当事件处理函数访问div变量时,形成循环引用,导致两个对象均无法回收,造成内存泄漏标记-清除算法
该算法引入“对象不再需要”和“对象不可访问(对象不可达)”概念。算法假设存在一系列根对象(JavaScript中根对象为全局对象),垃圾收集器定期从根对象开始,遍历其引用的对象,再遍历引用对象引用的对象,以此类推。通过此方式,垃圾收集器可获取所有可访问对象,并回收不可访问对象。
该算法优于前一种:引用为0的对象被设为不可访问,同时避免了循环引用带来的困扰。
截至2012年,多数现代浏览器采用“标记-清除算法”的垃圾收集器。尽管过去几年JavaScript垃圾收集领域(代/增量/并发/并行收集)改进了相关算法,但垃圾收集算法本身(标记-清除算法)及“如何判定对象不再需要”并未根本改善。
周期不再是问题
在第一个例子中,函数调用结束后,这两个对象不被全局对象引用,也不被全局对象引用的对象引用。因此,JavaScript垃圾回收器将其标记为不可访问对象。第二种情况同理,当div和事件处理函数被标记为不可访问时,即被释放。
限制:对象需明确标记为不可访问
这种标记方法存在局限,但开发者在编程中较少接触,因此通常不关注垃圾回收细节。
JS语法错误会阻止加载吗?区分SyntaxError与资源加载失败
2026-11-05 16:08:46
JavaScript常见SyntaxError语法错误排查与修复方法
2026-11-05 13:58:44
JS中slice、substring和substr截取字符串的区别
2026-11-05 13:43:47
JS 转 TS 手动迁移最佳实践:从配置 tsconfig 到消除 any 类型
2026-11-05 11:53:32
为什么JS数组push后得到的是数字而不是数组?
2026-11-05 09:59:18
