JavaScript内存管理详解:从生命周期到垃圾回收算法

作者:代码拾荒时间:2026-10-27 13:19:41浏览:352

本文详细解析JavaScript的内存生命周期,涵盖内存分配、使用及释放三个阶段。重点对比了引用计数与标记-清除两种垃圾回收算法的原理、局限性及实际应用,特别是针对循环引用导致的内存泄漏问题提供了深入分析,帮助开发者理解底层机制以优化代码性能。

JavaScript内存管理概述

介绍


在C等低级语言中,开发者需通过malloc()和free()等命令手动管理内存;而JavaScript作为高级语言,对象(如字符串、对象等)创建时自动分配内存,不再使用时由系统自动回收,这一过程称为垃圾回收。正因如此,许多JavaScript开发者误以为无需关注内存管理细节。


内存生命周期与分配机制

内存生命周期


无论何种编程语言,内存的生命周期通常遵循一致的模式。


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时,即认为该对象可被回收。


例子:


代码如下:

var o = {
a: {
b:2
}
}; // 创建了两个对象。一个对象(a)被另一个对象(o引用的对象)引用,并将a作为其属性
// 该对象又被变量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和事件处理函数被垃圾回收器标记为不可访问时,它们就会被释放。


限制:对象需要明确标记为不可访问


这种标记方法存在局限,但我们在编程中并未直接接触到它,因此很少关心垃圾回收的具体细节。

作者最新文章