【前端知识】JavaScript——垃圾回收机制
一、简述
JavaScript 通过自动内存管理实现内存分配和闲置资源回收,即垃圾回收机制。其基本思路是:确定哪个变量不会再使用,然后释放它占用的内存。垃圾回收的过程是周期性的,每隔一段时间就会自动运行。
二、两种标记策略
垃圾回收程序需要跟踪记录变量是否还需要使用,标记策略主要有两种:标记清理和引用计数。
-
标记清理(mark-and-sweep)
垃圾回收程序运行时,会标记内存中存储的所有变量。然后,将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后,若变量再被加上标记,则被视为待删除变量(因为任何在上下文中的变量都访问不到它们了)。随后,垃圾回收程序做一次内存清理,销毁带标记的所有值并回收内存。
-
引用计数
对每个值都记录它被引用的次数。当声明变量并赋引用值时,这个值的引用数为1。当值又被赋给另一个变量时,引用数加1。依此,不断累加。当变量被其他值覆盖时,则引用数减1。当引用数为0时,则表示该值不会再被访问了。垃圾回收程序会在下次运行时,释放引用数为0的值,回收内存。
引用计数策略存在循环引用的问题,当两个对象通过各自属性相互引用时,他们的引用数永远不会变成0,导致内存永远无法释放:
// 循环引用问题
let element = document.getElementById("some_element");
let myObject = new Object();
myObject.element = element;
element.someObject = myObject;
// 避免循环引用
// 把变量设置为 null 实际上会切断变量与其之前引用值之间的关系。当下次垃圾回收程序运行时,这些值就会被删除,内存也会被回收。
myObject.element = null;
element.someObject = null;
三、性能
垃圾回收的调度时机是浏览器决定的,并非开发者决定。我们可以通过优化代码来配合垃圾回收,从而提升程序性能。
-
解除引用
如果数据不再必要,那么把它设置为 null,从而释放其引用。
function createPerson(name){ let localPerson = new Object(); localPerson.name = name; return localPerson; } let globalPerson = createPerson("Nicholas"); // 解除 globalPerson 对值的引用 globalPerson = null;
-
通过 const 和 let 声明提升性能
const和 let 都以块(而非函数)为作用域,所以相比于使用 var,使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。
-
隐藏类和删除操作
V8 在将解释后的 JavaScript 代码编译为实际的机器码时会利用“隐藏类”。运行期间,V8 会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类的对象性能会更好,V8 会针对这种情况进行优化,但不一定总能够做到。
function Article() { this.title = 'Inauguration Ceremony Features Kazoo Band'; } let a1 = new Article(); let a2 = new Article(); // V8 会在后台配置,让这两个类实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原型。 // 若添加下列代码,则会导致两个 Article 实例对应两个不同的隐藏类。根据这种操作的频率和隐藏类的大小,这有可能对性能产生明显影响 a2.author = 'Jake'; // 解决方案: 避免 JavaScript 的“先创建再补充”(ready-fire-aim)式的动态属性赋值,并在构造函数中一次性声明所有属性 function Article(opt_author) { this.title = 'Inauguration Ceremony Features Kazoo Band'; this.author = opt_author; } let a1 = new Article(); let a2 = new Article('Jake'); // 使用 delete 关键字会导致生成相同的隐藏类片段,动态删除属性与动态添加属性导致的后果一样。即使两个实例使用了同一个构造函数,它们也不再共享一个隐藏类。 function Article() { this.title = 'Inauguration Ceremony Features Kazoo Band'; this.author = 'Jake'; } let a1 = new Article(); let a2 = new Article(); delete a1.author; // 最佳实践还是把不想要的属性设置为 null a1.author = null;
四、内存泄漏
JavaScript 中的内存泄漏大部分是由不合理的引用导致的。
function setName() {
name = 'Jake';
}
// 此时,解释器会把变量 name 当作 window 的属性来创建(相当于 window.name = 'Jake')。可想而知,在window 对象上创建的属性,只要 window 本身不被清理就不会消失。
let name = 'Jake';
setInterval(() => {
console.log(name);
}, 100);
// 只要定时器一直运行,回调函数中引用的 name 就会一直占用内存。垃圾回收程序当然知道这一点,因而就不会清理外部变量。
let outer = function() {
let name = 'Jake';
return function() {
return name;
};
};
// 调用 outer()会导致分配给 name 的内存被泄漏。以上代码执行后创建了一个内部闭包,只要返回的函数存在就不能清理 name,因为闭包一直在引用着它。
五、静态分配与对象池
-
对象池
在初始化的某一时刻,可以创建一个对象池,用来管理一组可回收的对象。应用程序可以向这个对象池请求一个对象、设置其属性、使用它,然后在操作完成后再把它还给对象池。由于没发生对象初始化,垃圾回收探测就不会发现有对象更替,因此垃圾回收程序就不会那么频繁地运
行。
-
静态分配
如果对象池只按需分配矢量(在对象不存在时创建新的,在对象存在时则复用存在的),那么这个实现本质上是一种贪婪算法,有单调增长但为静态的内存。这个对象池必须使用某种结构维护所有对象,数组是比较好的选择。不过,使用数组来实现,必须留意不要招致额外的垃圾回收。