垃圾回收机制

一、为什么要垃圾回收机制:

众所周知,应用程序在运行过程中需要占用一定的内存空间,且在运行过后就必须将不再用到的内存释放掉,否则就会出现内存的占用持续升高的情况,一方面会影响程序的运行速度,另一方面严重的话则会导致整个程序的崩溃。

二、JavaScript垃圾回收机制:

回收机制:

在js中创建一个变量时,会自动分配内存空间,当变量不再被使用时,垃圾回收机制会自动释放相应的内存空间

在垃圾回收机制中,我们还要注意一点,

全停顿:

垃圾回收算法在执行前,需要将应用逻辑暂停,执行完垃圾回收后再执行应用逻辑

三、JavaScript垃圾回收机制算法:

在讲解垃圾回收机制算法之前,我们先看一下JavaScript的判定垃圾的情况

JavaScript中会被判定为垃圾的情形如下:

  • 对象不再被引用;
  • 对象不能从根上访问到;

常见的垃圾回收算法有:

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

引用计数

统计引用类型变量声明后被引用的次数,当次数为0时,该变量将被回收。但是引用计数的算法有一个缺点:会出现循环引用。

引用计数的判断原理很简单,就是看一份数据是否还有指向它的引用,若是没有任何对象再指向它,那么垃圾回收器就会回收,其策略是跟踪记录每个变量值被使用的次数

  • 当声明了一个变量并且将一个引用类型赋值给该变量时,这个值的引用次数就为 1

  • 如果同一个值又被赋给另一个变量,那么引用数加 1

  • 如果该变量的值被其他的值覆盖了,则引用次数减 1

  • 当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存

这样说可能不是怎么好理解,我们通过例子来看一下

例子1

const user1 = {age: 11}
const user2 = {age: 22}
const user3 = {age: 33}
 
const userList = [user1.age, user2.age, user3.age]

user1、user2、user3都是被userList引用的,所以它们的引用计数是1,不为零,就不会被回收

例子2

function fn() {
    const num1 = 1
    const num2 = 2
}
 
fn()

fn函数执行完毕,num1、num2都是局部变量,执行过后,它们的引用计数就会减1,变成零,这样的代码就会被当做“垃圾”,进行回收。

例子3

function f() {
    var o1 = {};
    var o2 = {};
    o1.a = o2; // o1 引用 o2
    o2.a = o1; // o2 引用 o1
    return;
};

这里出现了循环引用,引用次数始终不会为0,因此垃圾回收器不会释放它们。

注:

引用计数算法其实还有一个比较大的缺点,就是我们需要单独拿出一片空间去维护每个变量的引用计数,这对于比较大的程序,空间开销还是比较大的。

引用计数算法优点:

  • 引用计数为零时,发现垃圾立即回收;
  • 最大限度减少程序暂停;

引用计数算法缺点:

  • 无法回收循环引用的对象;
  • 空间开销比较大;

标记清除法 

标记清除(Mark-Sweep),目前在 JavaScript引擎 里这种算法是最常用的,到目前为止的大多数浏览器的 JavaScript引擎 都在采用标记清除算法。

此算法可以分为两个阶段,一个是标记阶段(mark),一个是清除阶段(sweep)。

  • 标记阶段:垃圾回收器会从根对象开始遍历(在js中,通常认定全局对象window做为根)。每一个可以从根对象访问到的对象都会被添加一个标识,于是这个对象就被标识为可到达对象。可以理解为遍历所有对象找标记活动对象
  • 清除阶段:垃圾回收器会对堆内存从头到尾进行线性遍历,如果发现有对象没有被标识为可到达对象,那么就将此对象占用的内存回收,并且将原来标记为可到达对象的标识清除,以便进行下一次垃圾回收操作。可以理解为遍历所有对象清除没有标记对象,并回收相应空间

 但是标记清除法会导致内存碎片化。就是空闲内存块是不连续的,当需要多块连续的内存时,这些内存无法使用,就容易出现很多空闲内存块

标记整理

为了解决内存碎片化的问题,提高对内存的利用,引入了标记整理算法。

标记整理可以看做是标记清除的增强。标记阶段的操作和标记清除一致。

清除阶段会先执行整理,移动对象位置,将存活的对象移动到一边,然后再清理端边界外的内存。

清理后的结果如图:

标记整理的缺点是:移动对象位置,不会立即回收对象,回收的效率比较慢

增量标记

为了减少全停顿的时间,V8对标记进行了优化,将一次停顿进行的标记过程,分成了很多小步。每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成标记。

至于为什么会使用增量标记:

长时间的垃圾清理,会导致应用暂停和无响应,将会导致糟糕的用户体验。从2011年起,v8就将「全暂停」标记换成了增量标记。改进后的标记方式,最大停顿时间减少到原来的1/6。

四、v8引擎的垃圾回收机制

  • 采用分代回收的思想;
  • 内存分为新生代、老生代;

针对不同对象采用不同算法:
(1)新生代:对象的存活时间较短。新生对象或只经过一次垃圾回收的对象。
(2)老生代:对象存活时间较长。经历过一次或多次垃圾回收的对象。

新生代

新生代的特点:

  • 通常把小的对象分配到新生代
  • 新生代的垃圾回收比较频繁
  • 通常存储容量在1~8M

新生代-复制算法(Scavenge算法)

该算法将新生代分为两个等大空间,一空间叫做from(对象区域),另一空间叫做to(空闲区域),新加入的对象首先存放在from区域;

检查From空间内的存活对象,若对象存活,检查对象是否符合晋升条件,若符合条件则晋升到老生代,否则将对象从 From 空间复制到 To 空间。若对象不存活,则释放不存活对象的空间。完成复制后,将 From 空间与 To 空间进行角色翻转。

对象晋升

新生代对象晋升为老一代对象

当对象从From 空间复制到 To 空间时,若 To 空间使用超过 25%,则对象直接晋升到老生代中。设置为25%的比例的原因是,当完成 Scavenge 回收后,To 空间将翻转成From 空间,继续进行对象内存的分配。若占比过大,将影响后续内存分配。 

老生代

回收老生代对象主要采用标记清除标记整理增量标记算法,主要使用标记清除算法,只有在内存分配不足时,采用标记整理算法。

  1. 首先使用标记清除完成垃圾空间的回收
  2. 采用标记整理进行空间优化
  3. 采用增量标记进行效率优化

总结:

  • 新生代由于占用空间比较少,采用空间换时间机制。
  • 老生代区域空间比较大,不太适合大量的复制算法和标记整理,所以最常用的是标记清除算法,为了就是让全停顿的时间尽量减少

五、内存泄漏

查看内存泄漏的时候可以使用浏览器的Performance来监控内存变化

哪些情况容易引起内存泄漏

1. 全局变量

2. 闭包

3. 被遗忘的定时器或事件回调函数

注:

  • 全局变量等同于在window上添加属性,因此在函数执行完毕,依旧能够访问到它,因此不能够被回收
  • 闭包占用内存,不会清理

性能优化

1.避免使用全局变量

  • 全局变量会挂载在window下;
  • 全局变量至少有一个引用计数;
  • 全局变量存活更久,持续占用内存;
  • 在明确数据作用域的情况下,尽量使用局部变量;

2.减少判断层级

减少使用if,else

3.减少数据读取次数

对于频繁使用的数据,我们要对数据进行缓存。

4.减少循环体中的活动

在循环中减少活动的使用

5.事件绑定优化

尽量避免重复绑定相同的事件

6.避开闭包陷阱

闭包使用后,及时清空

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

萌新小吉

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

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

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

打赏作者

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

抵扣说明:

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

余额充值