JavaScript (十一) 垃圾回收与闭包
内存管理(Memory Management)
内存管理是指程序运行时的所需内存的管理。在编程语言中内存管理是在高级抽象层次上进行的,通常由编程语言的运行时环境或虚拟机来处理。它隐藏了底层计算机系统的细节,使开发人员能够更方便地分配和释放内存,而不需要考虑底层硬件和操作系统的具体实现
在编程语言中内存管理对于避免内存泄漏和提高程序性能至关重要。如果程序无法正确地管理内存,可能会导致内存泄漏、内存溢出和代码错误等问题
内存生命周期
内存生命周期是指程序运行时为其分配的内存从创建到最终释放所经历的过程,不管什么编程语言,内存生命周期基本是一致的,它可分为以下阶段:
- 分配内存(allocate memory):在程序中为变量、对象或某种数据结构分配内存空间
- 使用内存(use memory):将数据写入已分配的内存空间,并读取存储在其中的数据( 内存的读和写 )
- 释放内存(free up memory):不再需要此处内存时,将其返回/归还给操作系统以便其他程序使用
JavaScript 中的内存管理
1. 内存分配
JavaScript 在定义变量(对象,字符串等)时自动进行了分配内存( allocate memory )
例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 var n = 123; // 给数值变量分配内存
var s = "azerty"; // 给字符串分配内存
var o = {
a: 1,
b: null
}; // 给对象及其包含的值分配内存
// 给数组及其包含的值分配内存(就像对象一样)
var a = [1, null, "abra"];
function f(a){
return a + 2;
} // 给函数(可调用的对象)分配内存
var d = new Date(); // 分配一个 Date 对象
var e = document.createElement('div'); // 分配一个 DOM 元素2. 使用值
使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数
3. 当内存不在需要使用时释放
大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“哪些被分配的内存确实已经不再需要了”。它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它
高级语言解释器嵌入了“垃圾回收器”,它的主要工作是找到那些不在使用的内存,并自动将其释放。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)
垃圾回收(Garbage Collection)
垃圾回收( Garbage Collection )是指一种自动的内存管理机制。像 C/C++ 这样的底层语言一般都会提供相应的内存管理接口来手动管理内存,而 JavaScript/Python/Go 这类语言都有自己的垃圾回收机制来自动管理内存
垃圾回收的核心就是查找哪些对象在未来的程序执行中,将不会被访问,并释放这些对象所占用的内存
垃圾回收算法
垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个 JavaScript 对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)
引用计数(Reference Counting)
这是最初级的垃圾收集算法。它通过跟踪每个对象被引用的次数来确定何时可以释放对象所占用的内存
当一个对象被引用时,其引用计数值加一。当一个对象的引用被取消/删除时,其引用计数值减一。当引用计数值为零时,表示该对象没有被引用( 不再被使用 ),可以安全地释放其内存
例:
1 | var o = { |
限制:循环引用(Circular Reference)
该算法有个限制:无法处理循环引用的事例。在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收
例:
1
2
3
4
5
6
7
8 function f() {
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
}
f();
标记 - 清除(Mark and Sweep)
这个算法把“对象是否不再需要”简化定义为“对象是否可获得”( 对象可达性 )
标记清除算法主要包括两个阶段:标记阶段和清除阶段
在标记阶段,算法从根对象开始,遍历可访问的对象并标记为活动对象。在 JavaScript 里,根对象就是全局对象。这样,所有与根对象直接或间接相关的对象都会被标记为活动对象,而未被标记的对象则被认为是垃圾对象
在清除阶段,垃圾回收器会扫描整个堆内存,回收未被标记的垃圾对象所占用的内存空间,并将其释放供下次使用
标记清除算法是周期执行的,通常,在程序运行过程中,当达到一定条件(例如内存占用超过某个阈值)或特定时间间隔时,垃圾回收器会触发标记-清除算法进行垃圾回收操作
标记清除算法可以有效地找到并清除不再使用的垃圾对象,但它也有一些缺点。考虑到内存安全,标记清除算法必须停止应用程序的执行以进行垃圾回收操作,这可能会引起一定的停顿时间,对于实时应用或需要快速响应的系统来说,这可能会产生明显的延迟,并且可能影响用户体验。其次,标记清除算法在清除阶段需要遍历整个堆内存,这个过程的时间复杂度与存活对象的数量成正比,因此对于大规模的内存空间和活跃对象较多的程序,可能会带来较高的开销
从 2012 年起,所有现代浏览器都使用了标记 - 清除垃圾回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于标记 - 清除算法的改进,并没有改进标记 - 清除算法本身和它对“对象是否不再需要”的简化定义
循环引用不再是问题了
在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收
闭包(Closure)
闭包是 JavaScript 语言中强大而又神秘的特性之一
通常闭包是指一个能够访问自由变量的函数,这里的自由变量是指作用域外的变量。简单的说,闭包是一个内部函数,它可以访问外部函数作用域中的数据
例:
1 | function fn() { // 外部函数 |
在上面的案例中内部函数引用了外部函数的变量,并将内部函数返回到外部调用
即使外部函数执行结束,内部函数仍能访问外部函数的变量
目的
闭包的主要目的是实现状态的保留和封装。它提供了一种机制,可以在函数内部创建一个包含函数及其相关状态(变量)的封闭环境,并将其作为一个整体返回或传递给其他函数
优缺点
闭包可以防止变量或参数被外部污染( 变量只在闭包内可访问 )
闭包中的变量不会被垃圾回收( 变量持久化 )。如果变量一直被引用,无法被垃圾回收,可能会导致内存泄漏