likes
comments
collection
share

面试官:请你展开说说js中的垃圾回收机制

作者站长头像
站长
· 阅读数 10

前言

在现在的面试中,无论是大中小厂,垃圾回收机制已经是老生常谈的问题了,那么垃圾回收到底是什么?本篇文章继续针对这一问题展开探讨,如有不足的地方欢迎大家批评指正。

一、基础概念

(1)js内存的生命周期

1、 内存分配:当声明变量、函数、对象时,系统会自动分配内存给它们

2、 内存使用:即读写内存,也就是使用变量、函数

3、 内存回收:使用完毕,由垃圾回收器自动回收不再使用的内存

(2)什么是垃圾回收?

在说垃圾回收之前,我们首先需要了解的是,什么是垃圾?为什么要进行垃圾回收?

垃圾:  JS中的函数,变量,对象等都需要占用一定的内存,当这些东西不再被使用的时候,就变成了垃圾

  • 已经调用完毕的函数作用域及其内部的值
  • 值为 null 值
  • 无法被访问到的值

上面已经说了,JS中的所有的变量都会占用内存,当这些变量变成垃圾的时候,如果不进行回收,内存就会被一直占用,随着程序的运行,垃圾也会越来越多,总有一刻,内存会被占满,程序也就无法运行了

垃圾回收:  JavaScript中内存管理的重要组成部分。开发人员不需要手动分配和释放内存。垃圾回收机制可以自动处理内存的分配和释放,减轻了开发人员的负担,并且降低了内存泄漏的风险,它的主要目的是自动地检测和释放不再使用的内存,以便程序能够更高效地利用系统资源

(3)垃圾回收机制(Garbage Collection)简称GC

js的垃圾回收机制是一种内存管理技术,对开发者来说,js的垃圾回收机制是自动的、无形的。

我们创建的原始值、对象、函数……这一切都会占用内存,而在js引擎中有一个垃圾回收器在后台执行。

注意:全局变量一般不会回收(关闭页面时回收);一般情况下局部变量的值,不用了,会被自动回收掉。

二、已过时的垃圾收集算法 - 引用计数法

(1)引用计数法原理

主要看当前创建的对象在堆内存中有没有被其他对象引用,如果没有被其他对象引用,那么该对象将被垃圾回收机制回收。

let obj1 = { name: 'test'}; // 创建一个对象,被obj1所引用,计数为1
let obj2 = obj1; // obj2变量是第二个对该对象的引用,计数为2
obj1 = null; // 该对象的原始引用obj1已经没有了,计数为1
obj2 = null; // 此时对象所有引用都没有了,计数为0,垃圾回收机制回收该对象

(2)致命缺点:循环引用

两个对象内的属性相互引用对象,两个对象的引用计数永远大于0,无法回收导致内存泄漏。

function func(){
    let obj1 = {};
    let obj2 = {};
    obj1.a = obj2;
    obj2.a = obj1;
    return 1;
}

在2012年以后所有现代浏览器都取消这种算法了,取而代之的是标记清除法。

三、标记清除法(mark-and-sweep)

2012年起,所有现代浏览器都使用了标记清除法

(1)标记清除法原理

它会定期执行以下“垃圾回收”步骤:

例如,对象有如下的结构:

第一步,标记所有的根: 面试官:请你展开说说js中的垃圾回收机制

下面列出固有的可达值的基本集合, 这些值明显不能被释放。

比方说: 当前执行的函数,它的局部变量和参数。

当前嵌套调用链上的其他函数、它们的局部变量和参数。

全局变量。

(还有一些内部的)

这些值被称作 根 (roots)

面试官:请你展开说说js中的垃圾回收机制

第二步,跟随根的引用标记它们所引用的对象:

面试官:请你展开说说js中的垃圾回收机制

第三步,……如果还有引用的话,继续标记:

面试官:请你展开说说js中的垃圾回收机制

最后,通过这个过程没有被标记的对象被认为是不可达的,并且会被删除。

面试官:请你展开说说js中的垃圾回收机制

关于垃圾回收,js引擎做了许多优化,使垃圾回收运行速度更快,也使得现代浏览器的性能越来越高,并且不会对代码执行引入任何延迟。

四、分代回收(Generational Collection)

分代回收是一种结合了标记清除和引用计数的垃圾回收机制,它会根据对象的生命周期将内存分为不同的代。 关于两种分代回收的原理如下

老生代回收

老生代实际上就是上面说到的标记清除算法,这套算法适用于存活时间较长的对象 新生代回收

新生代堆被分为两个相等大小的区域:From空间和To空间

  1. 新对象分配到From空间
  2. 当From空间满时,触发垃圾回收
  3. 从根对象开始,标记所有存活的对象
  4. 将存活的对象复制到To空间中
  5. 清除已经死亡的对象
  6. 将To空间作为新的From空间,并将From空间作为新的To空间,完成垃圾回收

下面我使用JS实现一下新生代回收的过程

// 新生代回收机制
class GenerationalCollection {
  // 定义堆的From空间和To空间
  fromSpace = new Set();
  toSpace = new Set();
  garbageCollect(obj) {
    this.mark(obj); // 标记阶段
    this.sweep(); // 清除阶段
    // 切换From和To的空间
    const { to, from } = this.exchangeSet(this.fromSpace, this.toSpace);
    this.fromSpace = from;
    this.toSpace = to;
    return this;
  }
  isObj = (obj) => typeof obj === "object";
  exchangeSet(from, to) {
    from.forEach((it) => {
      to.add(it);
      from.delete(it);
    });
    return { from, to };
  }
  allocate(obj) {
    this.fromSpace.add(obj);
  }
  mark(obj) {
    if (!this.isObj(obj) || obj?.marked) return;
    obj.marked = true;
    this.isObj(obj) &&
      Reflect.ownKeys(obj).forEach((key) => this.mark(obj[key]));
  }
  sweep() {
    const { fromSpace, toSpace } = this;
    fromSpace.forEach((it) => {
      if (it.marked) {
        // 将标记对象放到To空间
        toSpace.add(it);
      }
      // 从From空间中移除该对象
      fromSpace.delete(it);
    });
  }
}
// 全局对象
const globalVar = {
    obj1: { name: "Object 1" },
    obj2: { name: "Object 2" },
    obj3: { name: "Object 3" }
}
const GC = new GenerationalCollection()
// 创建对象并分配到From空间
GC.allocate(globalVar.obj1)
GC.allocate(globalVar.obj2)
console.log(GC.fromSpace, GC.toSpace);
// 执行垃圾回收
GC.garbageCollect(globalVar)
console.log(GC.fromSpace, GC.toSpace);

简单描述一下上面的代码,allocate函数将对象放到From堆空间中,mark函数对对象及属性添加标记,在sweep清除函数中如果对象既被标记又在From空间中那么就将其复制到To空间中,最后在垃圾回收机制函数garbageCollect中对调两个堆空间最终完成整个周期

五、垃圾收集机制的一些优化建议

  • 分代收集(Generational collection)—— 对象被分成两组:“新的”和“旧的”。在典型的代码中,许多对象的生命周期都很短:它们出现、完成它们的工作并很快死去,因此在这种情况下跟踪新对象并将其从内存中清除是有意义的。那些长期存活的对象会变得“老旧”,并且被检查的频次也会降低。
  • 增量收集(Incremental collection)—— 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟。因此,引擎将现有的整个对象集拆分为多个部分,然后将这些部分逐一清除。这样就会有很多小型的垃圾收集,而不是一个大型的。这需要它们之间有额外的标记来追踪变化,但是这样会带来许多微小的延迟而不是一个大的延迟。
  • 闲时收集(Idle-time collection)—— 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。
转载自:https://juejin.cn/post/7280787122016059426
评论
请登录