【前端知识】JavaScript——垃圾回收机制

JavaScript的内存管理通过垃圾回收机制自动进行,主要策略包括标记清理和引用计数。然而,引用计数可能导致循环引用问题,解决方法是解除引用。性能优化可通过及时解除不再使用的变量引用、合理使用const和let以及避免动态属性赋值来实现。内存泄漏常见于全局变量、定时器回调和闭包。对象池和静态分配是优化内存使用的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【前端知识】JavaScript——垃圾回收机制

一、简述

​ JavaScript 通过自动内存管理实现内存分配和闲置资源回收,即垃圾回收机制。其基本思路是:确定哪个变量不会再使用,然后释放它占用的内存。垃圾回收的过程是周期性的,每隔一段时间就会自动运行。

二、两种标记策略

​ 垃圾回收程序需要跟踪记录变量是否还需要使用,标记策略主要有两种:标记清理和引用计数。

  1. 标记清理(mark-and-sweep)

    垃圾回收程序运行时,会标记内存中存储的所有变量。然后,将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后,若变量再被加上标记,则被视为待删除变量(因为任何在上下文中的变量都访问不到它们了)。随后,垃圾回收程序做一次内存清理,销毁带标记的所有值并回收内存。

  2. 引用计数

    对每个值都记录它被引用的次数。当声明变量并赋引用值时,这个值的引用数为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;

三、性能

垃圾回收的调度时机是浏览器决定的,并非开发者决定。我们可以通过优化代码来配合垃圾回收,从而提升程序性能。

  1. 解除引用

    如果数据不再必要,那么把它设置为 null,从而释放其引用。

    function createPerson(name){ 
     let localPerson = new Object(); 
     localPerson.name = name; 
     return localPerson; 
    } 
    let globalPerson = createPerson("Nicholas"); 
    // 解除 globalPerson 对值的引用
    globalPerson = null;
    
  2. 通过 const 和 let 声明提升性能

    const和 let 都以块(而非函数)为作用域,所以相比于使用 var,使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。

  3. 隐藏类和删除操作

    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,因为闭包一直在引用着它。

五、静态分配与对象池

  1. 对象池

    在初始化的某一时刻,可以创建一个对象池,用来管理一组可回收的对象。应用程序可以向这个对象池请求一个对象、设置其属性、使用它,然后在操作完成后再把它还给对象池。由于没发生对象初始化,垃圾回收探测就不会发现有对象更替,因此垃圾回收程序就不会那么频繁地运

    行。

  2. 静态分配

    如果对象池只按需分配矢量(在对象不存在时创建新的,在对象存在时则复用存在的),那么这个实现本质上是一种贪婪算法,有单调增长但为静态的内存。这个对象池必须使用某种结构维护所有对象,数组是比较好的选择。不过,使用数组来实现,必须留意不要招致额外的垃圾回收。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端Outman

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值