v8工作原理:垃圾回收,垃圾数据是如何自动回收的

前言

前面我说过v8是如何存储js的数据的,数据在代码执行完之后是要被销毁的,数据使用完之后不再被需要,我们把这样的数据称之为垃圾数据,如果这些垃圾数据一直保存在内存中,就会造成内存越使用越多,而得不到释放,我们就需要对这些垃圾数据进行回收,释放有限的内存空间

不同语言的垃圾回收策略

通常情况下,垃圾数据回收分为手动回收和自动回收两种策略.
如C/C++就是使用手动回收策略,何时分配内存,何时销毁内存都是由自己写的代码控制,如c语言就需要先调用mallco函数进行堆内存的分配,然后当不再使用这些数据时,就要进行free函数回收操作来销毁,否则就会发生内存泄漏
另外一种的自动回收策略就是由引擎自动回收了,如java/javascript.python等语言,产生的垃圾数据是由垃圾回收器来释放的,并不需要自己手动去释放

对于javascript而言,也正是因为相这个自动释放资源的特征带来了很多困惑,也让一些js开发者误以为可以不关心内存管理,这也是一个很大的误解

调用栈中的数据如何回收

首先是调用栈中的数据,我们还是通过一段代码实例的执行流程分析其回收机制

function foo() {
	var a = 1
	var b = { name: '凯隐' }
	function showName() {
		var c = '拉亚斯特'
		var d = { name: '拉亚斯特' }
	}
	showName()
}

我们知道原视类型时分配到栈中的,引用类型是分配到堆中的.当foo函数执行结束之后,foo函数的执行上下文会从堆中被销毁掉,那么它是怎么被销毁掉的呢?

我们可以知道,执行到showName函数时,js引擎会创建showName的执行上下文并压入栈,最终执行到showName函数时,此时会有记录当前执行状态的指针,我们称之为esp,指向调用栈的函数执行上下文,表示当前正在执行showName函数.接着,当showName函数执行完后,函数执行流程进入了foo函数执行上下文,那么此时就需要销毁showName的执行上下文了.esp这时候帮上忙了,js会将esp下移到foo函数执行上下文,这个下移操作就是销毁showName函数执行上下文的过程

当然这句话可能让人有点懵逼,我在这里说一下,当esp下移时,同时也是将顶层的函数执行上下文中的数据标记为无效内存,有闭包就加入闭包,做完这些过程之后,esp才会下移

堆中的数据如何回收

通过上面简单的了解,我们大致了解了一下回收流程,不过我们知道的是栈顶的执行上下文弹出后是无效状态,那么栈中保存的堆地址指向的堆内存呢?他们依然占用着空间

这时候就需要用到js中的垃圾回收器了

代际假说和分代收集

在学习v8如何实现回收之前,需要了解一下代际假说的内容,这是垃圾回收领域中一个重要的术语,后续垃圾回收的策略都是建立在该假说之上的

代际假说有以下两个特点

  • 第一个是大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;
  • 第二个是不死得对象,会活得很久

其实这两个特点不仅仅适用于javascript,同样适用于大多数得动态语言,如java,python等

有了代际假说得基础,我们就能具体探讨v8是如何实现垃圾回收的了

通常垃圾回收算法有很多种,但是并没有哪一种能胜任所有的场景,你需要权衡各种场景,根据对象的生存周期的不同而使用不同的算法,以便达到最好的效果

所以在v8中会把堆分钟新生代和老生代两个区域,新生代中存放时间生成极短的对象,老生代中存放生存时间久的对象.

新生代通常只支持1~8兆的容量,而老生代区支持的容量就大很多了.对于这两块区域,v8分别使用两个不同的垃圾回收器,以便更高效的实施垃圾回收

  • 副垃圾回收期,主要负责新生代的垃圾回收
  • 主垃圾回收期,负责老生代的垃圾回收

垃圾回收器的工作流程

v8将堆分成两个不同的区域–新生代和老生代,并分别使用两个不同的垃圾回收器.其实无论上面类型的垃圾回收器,他们都有一个共同的执行流程

第一步是标记空间中活动对象的和非活动对象.所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象

第二步就是回收非活动对象所占据的内存,其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象

第三步是做内存整理.一般来说,频繁的回收对象后,内存中就会存在大量不连续的空间,我们把这些不连续的内存空间称为内存碎片.当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况.所以最后一步需要整理这些内存碎片,但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片,比如哦我们说的js中的副垃圾回收期

那么接下来,我们就讲讲主副垃圾回收器是如何回收内存的

副垃圾回收器

副垃圾回收器主要负责新生区的垃圾回收,而通常情况下,大多数小的对象都会被分配到新生区,所以说这个区域虽然不大,但是垃圾回收器还是比较频繁的

新生代中用Scavenge算法来处理.所谓的Scavenge算法,是把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域.

新加入的对象都会存在到对象区域,当对象区域块被写满时,就需要执行一次垃圾清理操作

在垃圾回收过程中,首先需要对对象区域中的垃圾回收做标记,标记完成之后,就进入垃圾清理阶段,副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它会把这些对象有序的排列起来,所以这个复制过程,也就相当于完成了内存整理操作,复制到空闲区域就没有内存碎片了

完成复制后,对象区域与空闲区域进行角色转换.也就是原来的对象区变成了空闲区,空闲区变成了对象区.这样就完成了对象的垃圾回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复下去

由于新生代中采用的Scavenge算法,所以每次执行清理操作时,都需要将存活的对象从对象区复制到空闲区.但复制操作需要时间成本,如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区也设置的比较小

也正是因为新生区的空间不大,所以很容易被存活的对象装满整个区域,为了解决这个问题.js引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,就会被移动到老生区中

主垃圾回收器

主垃圾回收器主要负责老生区中的垃圾回收.除了新生区中晋升的对象,一些大的对象就会被直接分配到老生区.因此老生区中的对象有两个特点,一个是对象空间占用大,一个是对象存活时间长

由于老生区的对象比较大,若要在老生区中使用Scavenge算法进行垃圾回收就会花费太多的时间,从而导致回收执行效率不高,同时还会浪费一半的空间,因而,主垃圾回收器是采用标记-清楚算法进行垃圾回收的.下面我们来看看该算法是如何工作的

首先是标记过程.标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素就称为活动对象,没有到达的元素就可以判定为垃圾数据

比如最开始的那段代码,当showName函数执行完之后,esp向下移动,指向了foo函数的执行上下文,这时候如果遍历调用栈,是不会找到引用d变量那个地址的,也就意味着那块数据不可达也就是垃圾数据,被标记,而还能被调用栈访问到的对象称之为活动对象,被标记为活动对象.这就是大致的标记过程

接下来就是垃圾的清楚过程.它和副垃圾回收器的垃圾清除过程不同,你可以理解这个过程是清楚掉被标记为垃圾数据的对象,但是一块内存被标记清除算法处理过之后,会产生大量的不连续内存空间,也就是大量的内存碎片.这时候又诞生了一门处理该情况的算法–标记整理算法,这个标记过程仍然与标记清除算法一样,但是后续步骤不是直接对可回收对象进行处理,而是让所有存活的对象都向一段移动,然后直接清理掉端边界以外的内存,让内存变得连续

全停顿

现在大家已经知道了v8是使用副垃圾回收器和主垃圾回收器处理垃圾回收的,不过由于js是运行在主线程之上的,一旦执行垃圾回收算法都需要将正在执行的js脚本暂停下来,待垃圾回收完毕之后再恢复脚本执行.我们把这种行为称之为全停顿

比如堆中的数据有1.5g,v8实现一次完整的垃圾回收需要1秒以上的时间,这也是由于垃圾回收而引起js线程暂停执行的时间,若是这样的时间花销,那么应用的性能和响应能力都会直线下降.主垃圾回收器执行一次完整的垃圾回收流程如下

主线程: js代码->标记->清理->整理->js代码

在V8新生代的垃圾回收中,因其空间较小,且存活对象较少,所以全停顿的影响不大,但老生代就不一样了,如果执行垃圾回收的过程中,占用主线程过久,就像上面展示的流程一样,花费了200ms以上,在这200ms内,主线程是不能做其他事情的,比如页面正在执行一个js动画,因为垃圾回收器在工作,就会导致整个动画在这200ms内无法执行,给用户的感觉就是卡顿现象

为了降低老生代的垃圾回收而造成的卡顿,V8将标记过程分为一个个子标记过程,同时让垃圾回收标记和js应用逻辑交替执行,直到标记阶段完成,我们把这个算法称为增量标记算法.类似于cpu时间轮转片一样,使用增量标记算法,可以把一个完整的垃圾回收任务分为很多很小的任务,这些小的任务执行时间比较短,可以穿插在其他的js任务中间执行,这样当执行上述动画效果时,就不会让用户因为垃圾回收而感受到页面的卡顿了

总结

好啦,今天讲到这了
无论是垃圾回收的策略还是处理全停顿的策略,往往都没有一个完美的解决方案,你需要花一些时间来做权衡,而这需要牺牲当前某几方面的指标来换取其他几个指标的提升.

站在工程师的角度来看,我们需要在满足需求的前提下,权衡各个指标的得失,把系统做的尽可能适应最核心的需求

好了 这几天忙着考试 耽误了进展,后面后续都会一天一篇作用把自己的所学整理并且分享给大家的

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值