JavaScript内存管理机制与垃圾回收算法详解
本文详细解析JavaScript的内存生命周期,涵盖内存分配、使用与释放过程。重点介绍两种核心垃圾回收算法:引用计数及其导致的循环引用问题,以及标记清除算法如何解决不可达对象回收。通过具体代码示例说明IE6/7中的内存泄漏案例,帮助开发者理解自动内存管理的原理与局限。
JavaScript内存管理机制概述
开发者通常熟知Java、.NET或PHP等语言具备自动垃圾回收的内存管理机制,但往往忽略JavaScript同样拥有类似的机制。本文将深入剖析JavaScript垃圾回收的工作原理及其具体执行流程。
简介 在C等底层语言中,内存管理依赖malloc()和free()等显式调用。相比之下,JavaScript内置了垃圾回收(garbage collection)机制,由解释器自动完成内存的分配与回收。部分开发者误以为使用高级语言即可完全忽略内存管理,这种观点是错误的。
内存的生命周期与分配
内存的生命周期 尽管不同编程语言存在差异,但各类语言中内存的生命周期通常遵循相似的模式:
1. 在需要时分配内存 2. 对分配的内存进行读写操作 3. 当内存不再被需要时,将其释放 对于前两步,大多数语言的操作都是明确且直观的。然而,在JavaScript这类高级语言中,第三步(释放内存)的过程并不直观。
Javascript中分配内存空间 变量初始化 当执行变量初始化时,JavaScript会自动分配相应的内存空间(注:MDN文档中此处提及Value initialization,具体是声明时还是赋值时分配空间,仍需进一步学习确认)。
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'; //个人补充,未考证,这里会为someElement分配空间,如注释所说,为对象分配空间 }, false);
函数调用时候分配空间 某些函数调用会产生前文所述的“为对象分配空间”的情况。
var d = new Date(); var e = document.createElement('div'); // allocates an DOM element还有下面这种
var s = “azerty”; var s2 = s.substr(0, 3); // s2 is a new string // 由于Javascript中字符串是不可变的,所以Javascript也许并没有为s2中的字符串分配新空间,而是只存了[0, 3]的区间(用来索引)
var a = ["ouais ouais", "nan nan"]; var a2 = ["generation", "nan nan"]; var a3 = a.concat(a2); // 新的空间来存储数组a3
操作变量值 这部分主要涉及读取、写入及函数调用,操作相对简单。
垃圾回收算法原理与局限
内存不再被使用时,将它们释放掉 许多内存管理机制的难题都集中在此环节。最棘手的问题在于如何确认“这块内存空间已经不再需要”。这通常需要程序员明确告知程序,该内存已无用,请求回收。
而在高级语言的解释器中,嵌入了一个名为“垃圾回收(garbage collector)”的工具,用于跟踪内存的分配和使用情况,以便在内存不再需要时自动回收。然而,判断一块内存是否仍有用途具有不确定性,这意味着无法通过算法精确计算出所有情况。
垃圾回收 鉴于上述不确定性,垃圾回收机制采取了一种有限的解决方案。下面介绍几种垃圾回收算法的思想及其相应的局限性:
引用 该方法基于引用的概念。当变量a能访问对象A时,称A引用了a(无论是直接还是间接)。例如,一个JavaScript对象会引用其原型(间接引用)及其各个属性(直接引用)。
在这种情形下,对象的概念被扩展,除了原生对象外,还包含了函数的作用域链(或全局的词法作用域)。
引用计数 这是最朴素(naive)的垃圾回收算法。它将“可回收”的标准定义为“没有其他对象引用此对象”(原文:This algorithm reduces the definition of “an object is not needed anymore” to “an object has no other object referencing to it”)。也就是说,只有当对象没有任何引用时,才会被视为垃圾并回收。
举个例子 var o = { // 称之为外层对象 a: { //称之为内层对象 b:2 } }; // 创建了两个对象 内层对象作为外层对象的属性而被引用 // 而外层对象被变量o引用 // 显然,没有人会被垃圾回收
var o2 = o; // o2也引用了上面说的外层对象。好现在外层对象的引用计数为‘2' (被o和o2引用) o = 1; // 现在o不再引用外层对象,只有o2在引用,引用计数为 ‘1'
var oa = o2.a; // oa 引用内层对象 // 现在内层对象同时被作为外层对象的属性引用和被oa引用,引用计数为‘2'
o2 = “yo”; // 好,现在o2也不引用外层对象了,外层对象引用计数为“0” // 意味着外层对象可以被“垃圾回收”了 // 然而,内层对象还被oa引用着,因此还是没有被回收 (个人注释:这里有一点闭包的意味)
oa = null; // 现在oa不引用内层对象了 // 内层对象也被垃圾回收
局限:循环引用
看下面代码:
function f(){ var o = {}; var o2 = {}; o.a = o2; // o 引用 o2 o2.a = o; // o2 引用 o
return “azerty”; }
f(); // o o2两个对象构成了循环引用 // 当函数执行完毕的时候,他们就被关在了f的作用域里面,没有外面的人可以使用他们 // 所以按理说,他们已经没有存在价值了,需要被垃圾回收,释放内存 // 然而,他们的引用计数都不为“0” // 所以在这种引用计数的机制下,他们没有被回收
实际例子 在IE6和IE7版本的浏览器中,使用的是引用计数机制。因此,下面的代码在IE6和IE7中会导致稳定的内存泄漏:
var div = document.createElement("div"); div.onclick = function(){ doSomething(); }; // div的onclick属性,会引用 function // 然而这个 function 反过来又引用了这个div,因为div在handler的作用域里面。 // 造成上述循环引用,导致内存泄漏。标记清除算法
这种算法将“可回收”定义为“对象不可达”,即无法访问。
该算法会定义一个“根”对象,并定期从“根”出发,查找“根”下的所有对象,判断是否能从“根”找到一条路径引用到该对象。通过从不同的“根”出发,垃圾回收程序可以判断所有对象是否“不可达”。当对象被判定为“不可达”时,便会被回收。
这种算法比引用计数算法更优。因为“一个对象的引用计数是0”可以推出“这个对象不可达”,但逆命题不成立。也就是说,这种算法扩大了垃圾回收的范围。
循环引用不再是困扰 在上面的循环引用示例中,当函数返回时,o和o2已不再被任何人引用,即处于“不可达”状态,因此它们会被顺理成章地垃圾回收。
局限:对象需要明确的“不可达” 虽然这被视为一种局限,但在实际开发中这种情况很少发生,因此很少有人关注这一点。
JS动态构建复杂嵌套JSON对象的方法
2026-11-05 15:41:52
JS负数取余为什么是负数?详解余数与模运算区别
2026-11-05 13:34:41
JS 解压出现乱码或报错怎么解决?
2026-11-05 12:32:38
JS 中 startsWith、indexOf 与正则判断字符串前缀的对比
2026-11-04 22:19:43
JS解析Excel后怎么将数据转换为JSON格式
2026-11-04 20:58:28
