浏览器的垃圾回收机制

在前端开发中,内存管理是一个非常重要的领域。浏览器的垃圾回收机制(Garbage Collection, GC)是现代浏览器中的核心部分之一,它可以自动管理内存的分配与回收,帮助开发者避免手动管理内存,从而减少内存泄漏和性能问题的风险。

一、浏览器的垃圾回收机制

垃圾回收是一种自动内存管理机制,用于识别和释放不再使用的内存。垃圾回收的核心目标是释放不再使用的内存空间。在浏览器中,当页面中的某些对象不再被需要时,垃圾回收机制会自动将它们从内存中移除,确保不会发生内存泄漏。

JavaScript 是一种内存自动管理的语言,内存管理的核心就是垃圾回收。垃圾回收机制通过两种主要方式来检测和释放内存:

  1. 引用计数(Reference Counting):追踪每个对象的引用次数,当一个对象的引用次数为零时,就认为该对象不再使用,可以释放它占用的内存。
  2. 标记清除(Mark-and-Sweep):通过标记所有可达的对象,然后清除所有未标记的对象来回收内存。

引用计数曾经是早期垃圾回收机制中的一个重要方法,但由于其无法解决循环引用、性能开销较大以及不能有效解决内存碎片问题,现代浏览器和大多数编程语言已经转向使用更高效的垃圾回收策略,如 标记-清除算法分代回收增量回收并行回收 等。

二、垃圾回收机制的工作原理

1.引用计数

引用计数(Reference Counting)是一种较为基础的垃圾回收算法,它通过追踪对象的引用次数来判断对象是否可以被回收。每个对象都维护一个计数器,表示它被多少个其他对象引用。当一个对象的引用计数降为零时,表示没有任何引用指向该对象,可以安全地回收该对象的内存。

引用计数的工作原理

  1. 初始化引用计数:每当一个对象被创建并将其引用赋值给一个变量时,该对象的引用计数为 1。当其他变量引用该对象时,引用计数会增加;当该对象的引用被销毁或指向其他对象时,引用计数会减少。

  2. 对象引用计数增加与减少

    • 当一个对象被引用时(如赋值给其他变量或作为函数参数传递),它的引用计数增加。
    • 当一个引用被销毁时(如局部变量超出作用域或赋值为 null),该对象的引用计数减少。
  3. 回收垃圾对象:当一个对象的引用计数降到零时,意味着没有任何引用指向该对象,垃圾回收器会回收该对象占用的内存。

引用计数的示例

假设我们有以下 JavaScript 代码:

let obj1 = { name: "Object 1" };  // 引用计数为 1
let obj2 = obj1;                   // 引用计数为 2,因为 obj2 引用了 obj1
let obj3 = { ref: obj1 };          // 引用计数为 3,因为 obj3 引用了 obj1

obj2 = null;  // 引用计数为 2,obj1 的引用计数减少 1
obj3 = null;  // 引用计数为 1,obj1 的引用计数再次减少 1

obj1 = null;// 此时,obj1 的引用计数变为 0,垃圾回收器可以回收 obj1

在上述代码中,obj1 的初始引用计数为 1,后来通过 obj2obj3 增加了引用计数。当 obj2obj3,obj1 被设置为 null 时,obj1 的引用计数逐渐减为 0,垃圾回收器可以回收 obj1 占用的内存。

循环引用问题

引用计数的一个显著缺点是无法解决循环引用问题。例如,如果两个对象互相引用对方,它们的引用计数始终大于 0,即使它们不再被其他对象引用,垃圾回收器也无法将它们回收,从而导致内存泄漏。

示例:

复制代码
function createCircularReference() {
    let obj1 = { name: "Object 1" };
    let obj2 = { name: "Object 2" };
    obj1.ref = obj2;  // obj1 引用 obj2
    obj2.ref = obj1;  // obj2 引用 obj1
    return [obj1, obj2];
}

let circularObj = createCircularReference();
circularObj = null;  // 即使没有其他引用,obj1 和 obj2 依然互相引用,它们无法被回收
复制代码

在这种情况下,即使 circularObj 被设置为 null,因为 obj1obj2 互相引用,引用计数不会降到零,因此它们不能被回收。

2. 标记-清除

标记-清除算法是现代垃圾回收器(Garbage Collector, GC)中常用的内存回收算法,广泛应用于 JavaScript、Java、Python 等编程语言的垃圾回收机制。它的核心思想是通过标记存活对象,并清除未标记的对象来实现内存的回收。

标记-清除算法的基本流程可以分为两个阶段:标记阶段(Mark Phase)清除阶段(Sweep Phase)

  1. 标记阶段(Mark Phase)

    • 在这个阶段,垃圾回收器会从根对象(Root Object)开始遍历,标记所有可以访问到的对象为“活跃的”。
    • 根对象包括全局对象、当前执行栈上的局部变量、活动函数等。
    • 遍历所有可达的对象,并将这些对象标记为“活动”状态,意味着它们仍然被程序所引用。
  2. 清除阶段(Sweep Phase)

    • 在标记阶段完成后,垃圾回收器会检查堆中的所有对象。
    • 所有没有被标记为活动的对象(即不再被任何其他对象引用的对象)将被认为是垃圾,可以回收并释放内存。
    • 这时,垃圾回收器会删除这些不再需要的对象,释放它们占用的内存空间。

标记-清除算法的详细步骤

  1. 根对象标记:从程序的根对象(如全局对象、函数参数等)开始,递归或迭代地标记所有可以访问到的对象。
  2. 对象遍历:遍历所有对象,并检查每个对象的引用(即指向其他对象的引用)。如果对象是“活动的”,则标记它为“存活”。
  3. 清理垃圾对象:一旦完成标记阶段,垃圾回收器会遍历堆内存,找出所有未被标记的对象,并将其标记为“垃圾”。这些垃圾对象会被销毁,释放内存。

示例

下面是一个包含多个对象的 JavaScript 程序:

复制代码
function createObjects() {
    let obj1 = { name: 'Object 1' };
    let obj2 = { name: 'Object 2' };
    let obj3 = { name: 'Object 3' };
    
    obj1.ref = obj2;  // obj1 引用 obj2
    obj2.ref = obj3;  // obj2 引用 obj3
    
    // 假设程序中不再使用 obj1 和 obj3
    obj1 = null;  // 断开 obj1 和 obj2 的引用
    obj2 = null;  // 断开 obj2 和 obj3 的引用
}

createObjects();
复制代码

在上述代码中,obj1obj2obj3 都是对象,它们之间通过引用相互连接。在 createObjects 函数执行完后,obj1obj2 都被设置为 null,它们之间的引用被断开。

  1. 标记阶段:垃圾回收器从根对象(例如,函数调用栈、全局对象)开始,遍历所有活动对象。如果 obj1obj2 是活动的,它们会被标记为“活跃”对象。

  2. 清除阶段:由于 obj1obj2 已经被断开引用,它们不再可达,因此垃圾回收器会认为这些对象是垃圾并释放它们占用的内存。

3. 分代回收(Generational Collection)

分代回收基于一个观察:大多数对象的生命周期很短,只有少数对象会存活较长时间。因此,垃圾回收器将内存分为不同的代(Generation),并对不同代采用不同的回收策略。

  • 新生代(Young Generation):存放新创建的对象。新生代的垃圾回收频率较高,采用复制算法(Copying Algorithm)进行回收。

  • 老生代(Old Generation):存放存活时间较长的对象。老生代的垃圾回收频率较低,采用标记-清除或标记-整理(Mark-and-Compact)算法进行回收。

生成垃圾回收算法基于这样一个假设:大部分对象会很快变得不可达,因此,年轻代的对象会频繁进行垃圾回收。只有生命周期较长的对象才会进入老年代,老年代的回收相对较少,避免频繁回收带来的性能损耗。

参考文献:

posted @   雪旭  阅读(122)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示