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 内存优化策略
-
保持内存占用量小:
- 优化内存占用的最佳手段是确保只保存必要的数据。
- 不再需要的数据应设置为
null以解除引用,便于垃圾回收。 - 局部变量在超出作用域后会自动解除引用,但全局变量和全局对象的属性需要手动处理。
-
使用
const和let提升性能:- ES6引入的
const和let关键字不仅改善了代码风格,还有助于改进垃圾回收过程。 - 它们以块为作用域,相比
var可能更早地让垃圾回收程序介入,回收不再需要的内存。
- ES6引入的
-
隐藏类和删除操作:
- 某些JavaScript引擎(如V8)使用隐藏类来优化对象性能。
- 共享相同隐藏类的对象性能更好,因此应避免动态添加或删除属性。
- 将不想要的属性设置为
null而非使用delete,以保持隐藏类不变和继续共享。
-
避免内存泄漏:
- 意外的全局变量是最常见的内存泄漏问题,应使用
var、let或const声明变量。 - 定时器回调中的闭包也可能导致内存泄漏,应确保在不再需要时清除定时器。
- 使用闭包时要小心,避免创建不必要的引用链。
- 意外的全局变量是最常见的内存泄漏问题,应使用
-
静态分配与对象池:
- 减少对象更替速度可以降低垃圾回收频率,提升性能。
- 对象池是一种管理可回收对象的策略,可以复用对象而不是每次都创建新对象。
- 实现对象池时,应使用静态数组等结构来维护对象,并避免动态分配操作。
示例代码与解释
// 示例:解除全局变量引用
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; // 解除对对象的引用,便于垃圾回收