GC算法简介

  • GC就是垃圾回收机制的简写
  • GC可以找到内存中的垃圾、并且释放和回收空间

GC里面的垃圾是什么

  • 程序中不再需要使用的对象:

    function func() {
      name = 'ny'
      return `${name} is a coder`
    }
    func()

    从需求的角度来考虑:某一个数据,在使用完成之后,上下文里不再需要用到它了,就可以视为垃圾看待,比如上述代码的name

  • 程序里不能够再访问到的对象

    function func() {
      const name = 'ny'
      return `${name} is a coder`
    }
    func()

    从当前程序运行的过程中,某个变量能不能够再被引用到的角度来考虑:
    上述代码中,当func调用结束之后,外部空间就不再能访问const声明的name,所以说:当我么找不到它的时候,也可以算作是垃圾。

GC算法

  • GC是一种机制,垃圾回收器完成具体的工作
  • 工作的内容就是查找垃圾释放空间、回收空间
  • 算法就是工作时查找和回收所遵循的规则

常见的GC算法

  • 引用计数
  • 标记清除
  • 标记整理
  • 分代回收

引用计数算法实现原理

  • 核心思想:设置引用数,判断当前引用数是否为0
  • 引用计数器
  • 引用关系发生改变的时候,引用计数器会去主动的修改当前这个对象所对应的引用数值
  • 引用数值为0就立刻回收
const user1 = { age: 18 };
const user2 = { age: 19 };
const user3 = { age: 20 };

const nameList = [user1.age, user2.age, user3.age];

function func() {
  num1 = 1;
  num2 = 2;
}

func();

从全局的角度来考虑,window的下边是可以直接找到user1 user2 user3和nameList的,而函数func内部的num1和num2,由于我们没有设置关键字,同样是被挂载到当前的window下的,所以这些变量的引用数字都肯定不是0

而:

const user1 = { age: 18 };
const user2 = { age: 19 };
const user3 = { age: 20 };

const nameList = [user1.age, user2.age, user3.age];

function func() {
  const num1 = 1;
  const num2 = 2;
}

func();

我们给num1和num2声明了const关键字之后,意味着num1和num2只能在作用域内起效果,一旦func函数调用结束之后,我们在外部出发,就无法再找到num1和num2,这个时候他们俩的引用计数就是0,GC就会立即开始工作,把他们当成垃圾,立即执行对象回收,func执行结束之后,他们num1和num2内部所占的内存空间就会被回收掉。

而nameList内部也指向了user1、2、3的对象空间,所以脚本执行完之后,其实users的对象空间都还被引用着,所以引用计数器就不是0,就不会被视为垃圾,被回收。

总结:靠着当前对象身上的引用计数的数值,来判断是否为0,从而决定是否是一个垃圾对象

引用计数算法的优点:

  • 发现垃圾及时回收
    他可以根据当前对象的引用数值是否为0来决定这个对象是否是垃圾,如果找到了,就可以立即释放
  • 最大限度减少程序暂停
    应用程序在执行过程中,必然会对内存进行消耗,而当前的执行平台,内存肯定是有上限的,所以内存肯定有占满的时候。不过由于引用计数算法,他是时刻监控着引用数值为0的对象,所以我们可以认为,举一个极端的例子:当他发现内存即将饱满的时候,引用计数就会立马找到那些数值为0的对象空间对其进行释放,所以这样就保证了当前的内存是不会有占满的时候,这就是所谓的最大限度的减少程序暂停的说法。

引用计数算法的缺点:

  • 无法回收循环引用的对象
function fn() {
  const obj1 = {};
  const obj2 = {};

  obj1.name = obj2;
  obj2.name = obj1;

  return "";
}

fn();

虽然上面有提到内部作用域的变量在执行完之后就会被视为垃圾处理,但是在这里的代码中,obj1和obj2明显还有一个互相指引的关系,所以在这种情况下,引用计数器的数值并不是0,所以引用计数算法下的GC没有办法回收这两个空间了,这就造成了内存空间的浪费。

  • 时间开销大
    因为当前的引用计数他需要去维护一个数值的变化,所以在这种情况下,要时刻监控当前对象的引用数值是否需要修改,我们对象的数值修改需要消耗时间,如果内存里面有更多的对象需要修改,那么时间就会消耗的更多一些。

标记清除算法的实现原理

  • 核心思想:将整个垃圾回收操作分成两个阶段:标记和清除
  • 遍历所有的对象找到标记活动对象
  • 遍历所有对象清除没有标记的对象
  • 回收相应的空间

如图所示

  1. 标记阶段(Marking):
    标记根对象: 垃圾回收器首先从一组称为“根对象”的起始点开始,这些根对象是程序中直接可访问的对象,例如全局变量、栈中的变量等。

    递归标记: 从根对象开始,垃圾回收器递归地访问所有从根对象可达的对象,并在这个过程中给这些对象打上标记,表示它们是活动的对象(非垃圾)。

    标记完成: 当所有可达的对象都被标记后,标记阶段就完成了。此时,所有未被标记的对象被认为是垃圾。

2.清除阶段(Sweeping):

清除垃圾: 在清除阶段,垃圾回收器遍历整个堆(即内存空间),找到所有未被标记的对象,并将其释放。同时,也会将第一次所做的标记给清除掉。这样,被释放的内存就变成了可用的空间。
内存整理: 在清除阶段完成后,可能会存在一些不连续的内存块。为了提高内存的利用率,有些实现还会进行内存整理,将存活的对象移动到一起,形成连续的内存块。
最终还会把回收的空间放在一个空闲列表上,方便后续程序直接在这里申请空间使用。

标记清除算法的优点:

  • 相对于引用计数算法而言,有一个最大的优点就是可以解决对象循环引用的回收操作。标记清除算法能够处理循环引用的情况,因为它通过可达性分析,只回收不可达的对象,而不仅仅是计数。
  • 不需要实时处理: 标记清除算法不需要在对象引用发生变化时实时更新计数,而是通过定期的垃圾回收周期完成。

标记清除算法的缺点:

  • 空间碎片化

    比如我从全局可以找到A部分的内容,在标记清除算法中,B和C是不可达的,所以,在一轮清除之后,域内的内容被删除,B和C的空间被放在了空闲链表里,按照上面我们说的,这就会出现地址上不连续的内存块,一旦出现多了或者少了的数据要使用这些不连续的空间,就无法使用了。
  • 不能立即回收垃圾对象

标记整理算法的实现原理

  • 标记整理算法其实可以视作为标记清除算法的一个增强版
  • 标记阶段的操作和标记清除算法一模一样
  • 清除阶段会先执行整理,移动对象位置,产生连续的位置。



    如图所示,在回收之后, 如果我们后续想要申请空间,就可以最大化利用释放出来的空间。

这个算法,会配合着标记清除在V8引擎里配合着处理复杂的GC操作。

优先:减少碎片化空间
缺点:不能立即回收垃圾对象

最后修改:2024 年 03 月 21 日
收款不要了,给孩子补充点点赞数吧