JavaScript 垃圾回收机制

0.何为垃圾回收

看下面例子:

let test = { name: "codereasy" }; 

test = [1,2,3,4,5]

 在JavaScript中,引用类型保存在堆内存中,然后在栈内存中保存一个对堆内存中实际对象的引用。在上述例子中,我们声明了一个变量 test,它引用了对象  { name: "codereasy" } 

 

接着我们给这个变量重新赋值了 一个数组,于是 test 指向了数组 [1,2,3,4,5],这就导致 test 不再指向之前的对象了。 没有了引用关系, { name: "codereasy" } 也就变成了无用的对象,所以就需要被清理(回收)。

1.引用计数法

跟踪记录每个对象的引用次数,当一个对象的引用次数为0时,说明这个对象没有被其他对象引用,就可以被安全地回收了。

let object1 = {name: 'Object 1'};
let object2 = {name: 'Object 2'};
let object3 = {name: 'Object 3'};

object1.otherObject = object2;
object2.otherObject = object3;

在这个例子中,我们创建了三个对象:

 {name: 'Object 1'}

 {name: 'Object 2'}

 {name: 'Object 3'}

在引用计数的角度来看,{name: 'Object 1'} 的引用次数为1(只有 let object1 引用了它),{name: 'Object 2'} 的引用次数为2(首先,它被 let object2 引用,其次,它被object1.otherObject引用 )

现在,如果我们删除 object1.otherObject 对 {name: 'Object 2'} 引用

object1.otherObject = null;

那么 {name: 'Object 2'} 的引用次数变为 1,此时它还不会被销毁。如果继续将 let object2 对它的引用删除:

 let object2 = null

{name: 'Object 2'} 就会成为一个无法访问的对象,可以被垃圾回收器回收。

这种方法看起来没问题,但是遇到循环引用的时候就会出错:

let objectA = {name: 'Object A'};
let objectB = {name: 'Object B'};

objectA.otherObject = objectB;
objectB.otherObject = objectA;

// 删除对objectA和objectB的引用
objectA = null;
objectB = null;

即使我们将 objectA 和 objectB 的引用全部删除,它们的引用计数仍然为1(因为它们互相引用),所以垃圾回收器不会将它们回收,导致内存泄漏。

想要进行垃圾回收,必须断开循环引用

objectA.otherObject = null;
objectB.otherObject = null;

所以无法清楚循环依赖是它的缺点,那么它的优点是什么呢?

引用计数算法的优点我们对比标记清除来看就会清晰很多,首先引用计数在引用值为 0 时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾。

而标记清除算法需要每隔一段时间进行一次,那在应用程序(JS脚本)运行过程中线程就必须要暂停去执行一段时间的 GC,另外,标记清除算法需要遍历堆里的活动以及非活动对象来清除,而引用计数则只需要在引用时计数就可以了

2.标记-清除法

此算法分为两个阶段:标记阶段清除阶段

在标记阶段,垃圾回收器会遍历所有的对象,对所有存活的对象进行标记。所谓的"存活",就是说在当前的执行上下文中,这个对象仍然有被引用。这种引用可以是直接的,也可以是间接的。

标记阶段的具体做法:从根(root)出发(根是全局对象),遍历所有的可达对象。所谓可达对象,就是说我们通过一些方式能到达这个对象,比如说一个对象作为函数的参数,在函数体内部可以访问到,或者一个对象作为另一个对象的属性,我们可以通过引用链找到它。遍历的过程中,GC会给所有找到的、从根出发可以访问到的对象标记上活动对象(reachable/alive)的标记。

在清除阶段,垃圾回收器会再次遍历所有的对象,对于没有被标记为"存活"的对象,进行回收。

以下是标记的过程:

// 全局变量可以被视为根,因此它们是可达的
let globalVariable = {
  name: "I'm a global variable",
};

// 函数的参数和内部变量也是可达的
function someFunction(someArgument) {
  let functionVariable = {
    name: "I'm a variable inside a function",
  };
  console.log(someArgument, functionVariable);
}

someFunction(globalVariable);

// 对象的属性也是可达的,因为它们可以通过对象引用链从根访问
let objectA = {
  objectProperty: {
    name: "I'm a property of an object",
  },
};

console.log(objectA.objectProperty);

然后回到引用计数法出现问题的代码:

let objectA = {name: 'Object A'};
let objectB = {name: 'Object B'};

objectA.otherObject = objectB;
objectB.otherObject = objectA;

引用计数法中,objectA和objectB都不会被回收。但如果我们使用标记清除法,当我们从根对象开始进行标记时,如果 objectAobjectB 无法从根对象访问到(即它们被完全隔离,没有任何从根对象开始的引用链可以到达它们),那么即使 objectAobjectB 互相引用,标记清除垃圾回收器也不会给它们打上活动标记。

在清除阶段,因为 objectAobjectB 都没有被标记为活动对象,垃圾回收器会认为它们是垃圾,然后回收它们所占用的内存,从而解决了循环引用的问题。

标记清除法的缺点

清除步骤结束之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了 内存碎片(如下图),并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题。

假设我们现在需要分配一块大小为 size 的内存空间,应该如何寻找它呢?

 一般情况下,我们有三种策略:

  • First-fit,找到大于等于 size 的块立即返回

  • Best-fit,遍历整个空闲列表,返回大于等于 size 的最小分块

  • Worst-fit,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分 size 大小,并将该部分返回

不管是哪种算法,都面临以下缺点:

  • 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块
  • 分配速度慢,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢

参考资料:https://juejin.cn/post/698158827635631721

「硬核JS」你真的了解垃圾回收机制吗 - 掘金 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

codereasy

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

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

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

打赏作者

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

抵扣说明:

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

余额充值