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

作者:小括号

时间:2026-10-29

浏览:464

本文系统介绍JavaScript的内存生命周期,涵盖内存分配、使用及释放三个阶段。重点解析引用计数与标记-清除两种垃圾回收算法的原理、局限性及实际表现,特别是针对循环引用导致的内存泄漏问题提供深入分析,帮助开发者理解V8引擎等现代浏览器的内存管理策略。

文章目录

JavaScript内存管理概述

简介

与C等低级语言提供malloc()、free()等底层内存管理基元不同,JavaScript的内存分配与释放是自动化的。当变量(如对象、字符串等)被创建时,系统会自动分配内存;当这些变量不再被使用时,系统会“自动”释放其占用的内存,这一过程称为垃圾回收(Garbage Collection)。这种自动化机制容易给JavaScript及其他高级语言开发者造成一种错觉,即可以完全忽略内存管理问题。

内存生命周期

内存生命周期

无论使用何种编程语言,内存的生命周期通常遵循以下基本流程:

1. 分配程序运行所需的内存空间。 2. 使用分配的内存,包括读取和写入操作。 3. 当内存不再需要时,将其释放。这一过程与“把大象装进冰箱”的步骤逻辑相似。

前两个步骤在所有编程语言中都是明确且清晰的。然而,在C等低级语言中,内存释放也是由程序员显式控制的,因此这一步骤也很清晰;但在JavaScript等高级语言中,内存释放是由系统自动完成的,因此这一步骤对开发者而言是不透明的。

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 is a new string
//因为string是不变量,JavaScript可能没有分配内存,但只是存储了0-3的范围。
var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2); // 新数组中有连接数组a和数组a2中的四个元素。

值的使用

使用值的过程本质上是对已分配内存进行读取和写入的操作。这意味着可以向变量或对象属性写入值,也可以将值作为参数传递给函数。

内存释放与垃圾回收

当内存不再需要使用时释放

大多数内存管理问题都发生在这一阶段。最困难的任务是确定“所分配的内存确实已经不再需要”。这通常要求开发人员识别程序中哪些内存块已不再使用并手动释放它们。

高级语言解释器中嵌入了“垃圾回收器”,其主要任务是跟踪内存的分配和使用情况,以便在分配的内存不再使用时自动释放它。这个过程是近似的,因为判断某块内存是否仍然需要是“不可判定的”(即无法通过某种算法完全解决)。

垃圾回收

如前所述,自动判断内存是否“不再需要”是一个不可判定问题。因此,垃圾回收的实现只能以有限的方式解决一般性问题。本节将解释相关概念,介绍主要的垃圾回收算法及其局限性。

引用

垃圾回收算法主要依赖于“引用”的概念。在内存管理环境中,如果一个对象有权访问另一个对象(无论是隐式还是显式),则称该对象引用另一个对象。例如,一个JavaScript对象具有对其原型的隐式引用,以及对其属性的显式引用。

在这里,“对象”的概念不仅特指JavaScript对象,还包括函数作用域(或全局词法作用域)。

引用计数垃圾收集算法

引用计数垃圾收集

这是最简单的垃圾收集算法。该算法将“对象是否不再需要”简化定义为“对象是否有其他对象引用到它”。如果没有引用指向该对象(即零引用),该对象将被垃圾回收机制回收。

例如

代码如下:

var o = {
a: {
b:2
  }
}; 
// 两个对象被创建,一个做为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集
var o2 = o; // o2变量是第二个对“这个对象”的引用
o = 1; // 现在,“这个对象”的原始引用o被o2替换了
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 "azerty";
}
f();
// 两个对象被创建,并互相引用,形成了一个循环
// 他们被调用之后不会离开函数作用域
// 所以他们已经没有用了,可以被回收了
// 然而,引用计数算法考虑到他们互相都有至少一次引用,所以他们不会被回收

实际当中的例子

IE 6, 7 对DOM对象进行引用计数回收。对他们来说,一个常见问题就是内存泄露:

代码如下:

var div = document.createElement("div");
div.onclick = function(){
doSomething();
}; 
// div有了一个引用指向事件处理属性onclick
// 事件处理也有一个对div的引用可以在函数作用域中被访问到
// 这个循环引用会导致两个对象都不会被垃圾回收

标记-清除算法

标记-清除算法

这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。

这个算法假定设置一个叫做根的对象(在Javascript里,根是全局对象)。定期的,垃圾回收器将从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和所有不能获得的对象。

这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。

从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。

循环引用不再是问题了

在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。 第二个示例同样,一旦 div 和其事件处理无法从根获取到,他们将会被垃圾回收器回收。

限制: 对象需要明确的不可获得

尽管这是一个限制,但是很少会被突破,这也就是为什么在现实中很少人会去关心垃圾回收机制。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章