第 4 章 变量、作用域与内存(4.3 垃圾回收)

123 阅读5分钟

4.3 垃圾回收

JavaScript 通过自动内存管理来实现内存分配和闲置资源回收,为开发者减轻了手动管理内存的负担。基本思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。

4.3.1 标记清理(Mark-and-Sweep)

标记清理是 JavaScript 最常用的垃圾回收策略。当变量进入执行上下文时,它们会被标记为“可达”或“活动”。当变量离开上下文或不再被引用时,它们会被标记为“不可达”或“垃圾”。垃圾回收器会定期遍历所有对象,并删除那些被标记为“垃圾”的对象。

例子

在函数 problem() 中声明的局部变量 objectA 和 objectB 在函数执行期间是可达的。但是,当函数执行完毕后,这两个变量就不再被引用,因此它们会被标记为“垃圾”,并在下次垃圾回收时被删除。

function problem() { 
    let objectA = new Object(); 
    let objectB = new Object(); 
    objectA.someOtherObject = objectB; 
    objectB.anotherObject = objectA; 
    // 函数结束后,objectA 和 objectB 不再被引用
}

在标记清理策略下,即使 objectA 和 objectB 相互引用,它们也会在函数结束后被正确地回收。

4.3.2 引用计数(Reference Counting)

另一种垃圾回收策略是引用计数(reference counting)。这一策略的基本思路是对每个值都记录它被引用的次数。当声明变量并给它赋一个引用值时,这个值的引用数会增加。如果同一个值又被赋给另一个变量,那么引用数会再加一。相反,如果保存对该值引用的变量被其他值覆盖,那么引用数会减一。当对象的引用计数变为 0 时,该对象就可以被回收。

例子

然而,引用计数存在一个严重的问题:循环引用。以下是一个循环引用的例子:

function problemFunction() {
    let objectA = new Object();
    let objectB = new Object();
    objectA.someOtherObject = objectB;
    objectB.anotherObject = objectA;
    // objectA 和 objectB 相互引用,引用数均为 2
}

在引用计数策略下,如果两个对象相互引用,那么它们的引用数永远不会变为 0,即使它们实际上已经不再被使用。这会导致内存泄漏问题。此外,在早期的 IE 版本中(如 IE8 及更早版本),BOM 和 DOM 对象是使用 C++ 实现的 COM 对象,并且这些对象使用引用计数进行垃圾回收。因此,即使 JavaScript 引擎使用标记清理策略,涉及 COM 对象的循环引用仍然会导致内存泄漏。

循环引用问题及解决方案

let element = document.getElementById("some_element");
let myObject = new Object();
myObject.element = element;
element.someObject = myObject;
// 造成了循环引用
 
// 解决方案:切断原生 JavaScript 对象与 DOM 元素之间的连接
myObject.element = null;
element.someObject = null;

通过将变量设置为 null,可以切断它们与其之前引用值之间的关系。这样,在下次垃圾回收程序运行时,这些值就会被删除,内存也会被回收。

4.3.4 内存管理

在使用垃圾回收的编程环境中,虽然开发者通常无需直接关心内存管理,但在JavaScript这种运行在浏览器中的语言里,内存管理仍是一个需要注意的问题。浏览器的内存限制比桌面软件更为严格,这主要是出于安全考虑,以防止网页耗尽系统内存导致崩溃。

4.3.5 内存优化策略

  1. 保持内存占用量小

    • 优化内存占用的最佳手段是确保只保存必要的数据。
    • 不再需要的数据应设置为null以解除引用,便于垃圾回收。
    • 局部变量在超出作用域后会自动解除引用,但全局变量和全局对象的属性需要手动处理。
  2. 使用constlet提升性能

    • ES6引入的constlet关键字不仅改善了代码风格,还有助于改进垃圾回收过程。
    • 它们以块为作用域,相比var可能更早地让垃圾回收程序介入,回收不再需要的内存。
  3. 隐藏类和删除操作

    • 某些JavaScript引擎(如V8)使用隐藏类来优化对象性能。
    • 共享相同隐藏类的对象性能更好,因此应避免动态添加或删除属性。
    • 将不想要的属性设置为null而非使用delete,以保持隐藏类不变和继续共享。
  4. 避免内存泄漏

    • 意外的全局变量是最常见的内存泄漏问题,应使用varletconst声明变量。
    • 定时器回调中的闭包也可能导致内存泄漏,应确保在不再需要时清除定时器。
    • 使用闭包时要小心,避免创建不必要的引用链。
  5. 静态分配与对象池

    • 减少对象更替速度可以降低垃圾回收频率,提升性能。
    • 对象池是一种管理可回收对象的策略,可以复用对象而不是每次都创建新对象。
    • 实现对象池时,应使用静态数组等结构来维护对象,并避免动态分配操作。

示例代码与解释

// 示例:解除全局变量引用
function createPerson(name) {
    let localPerson = new Object();
    localPerson.name = name;
    return localPerson;
}
let globalPerson = createPerson("Nicholas");
// 解除 globalPerson 对值的引用
globalPerson = null;
 
// 示例:使用 const 和 let
function createArticle(author) {
    const title = 'Inauguration Ceremony Features Kazoo Band';
    let article = { title, author };
    return article;
}
let a1 = createArticle();
let a2 = createArticle('Jake');
 
// 示例:避免动态添加属性
function Article(opt_author) {
    this.title = 'Inauguration Ceremony Features Kazoo Band';
    this.author = opt_author || null;
}
let article1 = new Article();
let article2 = new Article('Jake');
 
// 示例:使用对象池(伪代码)
let vectorPool = {
    vectors: [],
    allocate: function() {
        if (this.vectors.length === 0) {
            // 如果没有可用对象,则创建一个新对象
            return new Vector();
        } else {
            // 复用已有对象
            return this.vectors.pop();
        }
    },
    free: function(vector) {
        // 将对象放回对象池
        this.vectors.push(vector);
    }
};
 
// 使用对象池
let v1 = vectorPool.allocate();
let v2 = vectorPool.allocate();
v1.x = 10; v1.y = 5;
v2.x = -3; v2.y = -6;
function addVector(a, b, resultant) {
    resultant.x = a.x + b.x;
    resultant.y = a.y + b.y;
    return resultant;
}
let v3 = vectorPool.allocate();
addVector(v1, v2, v3);
console.log([v3.x, v3.y]); // [7, -1]
vectorPool.free(v1);
vectorPool.free(v2);
vectorPool.free(v3);
v1 = v2 = v3 = null; // 解除对对象的引用,便于垃圾回收