V8垃圾回收机制-速通[通俗易懂]

542 阅读7分钟

前言

今天废话不多,全是干货,和大家聊一聊v8的垃圾回收机制是怎样的。

首先大家需要搞清楚堆和栈的概念,原始数据类型放在栈中,引用数据类型放在堆内,包括v8读代码,在函数调用之前会进行的预编译的过程,全局执行上下文,函数执行上下文的入栈过程。

弄清楚了这些你就能清晰的明白下面的代码的执行结果是什么

function foo(){
    var a = 1
    var b = a
    a = 2
    console.log(a)
    console.log(b)
}

function foo(){
    var a = {name:'kk'}
    var b = a
    a.name = 'zykk'
    console.log(a)
    console.log(b)
}

image.png 那么如果是引用数据类型,大家也就知道了,引用地址会指向堆中,最终a和b共用了一个引用地址。

垃圾回收

弱类型:

js是弱类型的语言,如何理解?

  • 声明变量时不需要告诉v8该变量是什么类型,v8会自己计算出来

动态语言:

  • 所谓动态语言其实就是可以使用同一个变量保存不同类型的数据

那么什么是垃圾回收?我们知道,一个原始数据是存放在栈中的,一个引用类型数据是存放在堆当中的,通过这种分配方式我们就解决了数据分配的问题,但是数据使用之后就不再需要了,这个数据我们称为垃圾数据。如果这些垃圾数据一直保存在内存中,这些内存占用就越来越多,造成内存泄漏。因此我们需要一个垃圾回收机制来释放有限的内存空间

不同的语言有不同的垃圾回收机制,通常情况下,垃圾回收分为手动回收自动回收两种策略

手动回收: 例如c语言

char* p = char(*)malloc(2048)
free(p)
p = NULL

自动回收: JS/JAVA/PYTHON

function foo(){
    var a = 1
    var b = { name: 'kk'}
    function showName(){
        var c = 2
        var d = { name:'zykk'}
    }
    showName()
}
foo()
 

image.png 当showName调用完毕,,ESP指针下移,V8执行到foo上,此时如果showName下面还有其他的执行上下文,这个执行上下文会覆盖showName执行上下文。Esp是v8维护的用来标识当前执行状态的指针,指向showName表示正在执行showName函数。

栈内存回收

当一个函数执行结束之后,js引擎会通过下移esp指针来销毁该函数保存在栈当中的执行上下文,那么这个过程就叫做栈内存回收

堆内存回收

要回收堆当中的垃圾数据就需要用到js当中的垃圾回收器

要了解垃圾回收是个什么东西,首先要了解一下一个玄学代际假说/分代收集

物理界数学界有个假说:只要物体的运行速度超过了光速,你就可以突破时间的禁锢进行时间旅行,在科学界内有个假说是我们认为在计算机中,大部分对象在内存中存在的时间都很短,我们没办法人为控制对象在计算机当中存活的时间,当然某些情况下可以人为控制。简单来说就是我们不知道一个对象在内存中能存活的时间多长

代际假说认为:大部分对象在内存中存在的时间是很短的/如果一个对象存活的时间很长我们说这是一个不死的对象,就像修仙需要渡劫,渡劫成功后就能活千年万年,否则就是神形俱灭。这两个说法不仅仅适用于js还适用于大多数的动态语言例如java/python。

有了代际假说之后我们就可以来探讨v8是如何做垃圾回收机制的。通常来说垃圾回收是一个非常强大的算法,如果将来你有幸入职google,你就会知道。

但是算法有很多种,没有哪一个算法能胜任所有的场景,浏览器环境也好还是其他的环境,任何一个环境当他需要垃圾回收机制的时候他应该都提前引入好了很多的算法,这里就需要去权衡各个场景,根据对象的生命周期的不同来使用不同的算法最终达到最好的效果。

在v8堆中其实被划分成了两个区域,一个新生代一个老生代,新生代区域用来存放生存时间短的对象反之老生代区域。新生代区域通常只有1~8M,而老生代区域的内存容量就很大,因为我们说堆的内存容量本身是没有上限的,你的计算机有多大内存堆的内存就有多大。

对于这两个区域,v8使用了两个不同的垃圾回收器来回收以便实现更高效的回收

新生代的垃圾回收器叫副垃圾回收器

老生代叫主垃圾回收器

垃圾回收器的工作流程

  1. 标记空间当中活动对象和非活动对象,非活动对象指的就是这个对象已经用完了
  2. 回收掉非活动对象的内存
  3. 内存整理,清理内存碎片

这个机制是一个神鬼莫测的机制,没人知道他在什么时候生效,可能代码运行的好好的突然就生效一下,佛西/随缘。

新生代区域会划分对象区域和空闲区域,将对象区域中的活动对象复制到空闲区域,清空空闲对象,然后反转对象区域和空闲区域

经过两次反转后还存活的对象就晋升到老生代区域

老生代区域会循环递归的标记每一个对象中存活的子对象,清理失活的对象,整理内存

image.png

全停顿

那么什么是全停顿?

  • 这是垃圾回收机制工作的时候一定会带来的一个问题,因为垃圾回收机制是使用副垃圾回收机制和主垃圾回收机制的,但是由于js是运行在主线程上面的,v8运行在主线程上面,此时渲染线程等等线程就不能工作,所以一旦垃圾回收机制开始生效,此时js代码就会暂停下来,但是垃圾回收机制什么时候生效又没有人知道,不受程序员控制。

假设堆里面存了1.5g数据,假设v8实现一次垃圾回收需要1s,那么由于垃圾回收而引起的js执行暂停的时间就要1s或者更多,那么这样的时间开销就会导致js语言的性能就会直线下降

全停顿会造成页面的卡顿,解决方案:增量标记法,降低老生代垃圾回收机制的卡顿

image.png 其实就是把垃圾回收机制的时间零零散散的分散,不是一次性执行。

小结

JavaScript中的垃圾数据回收机制是通过副垃圾回收器和主垃圾回收器来实现的。副垃圾回收器主要负责新生区的垃圾回收,采用Scavenge算法将新生代空间对半划分为对象区域和空闲区域,通过复制和角色翻转完成垃圾数据的回收。而主垃圾回收器则负责老生区的垃圾回收,采用标记-清除和标记-整理算法来处理大对象和长生命周期对象的回收。在执行垃圾回收时,由于JavaScript运行在主线程上,会导致全停顿现象,影响应用的性能和响应能力。为了降低全停顿的影响,V8引擎采用增量标记算法来分解垃圾回收任务,使其与JavaScript应用逻辑交替执行,从而减少页面卡顿现象