一文带你搞懂V8垃圾回收系统
作者:荣达
内存中两种变量的存储
在V8中,JavaScript的内存空间分为栈(Stack)和堆(Heap)两部分。栈用于存储原始类型,如Number,String,Boolean,Null,Undefined,Symbol以及引用对象的内存地址,而堆用于存储引用类型的对象。
垃圾回收的基本思路是:查找内存中的所有变量,看哪些已经不再需要,然后释放这些变量所占用的内存。
所以V8中的垃圾回收可以分为栈回收和堆回收。
栈回收
在V8引擎中,函数调用的参数、返回地址和局部变量都存储在调用栈中。每当一个函数被调用时,都会创建一个新的栈帧,其中包含这些信息。而栈帧的回收则非常直接:一旦函数调用结束,其栈帧就会被立即移除。这种机制依赖于ESP(Extended Stack Pointer)指针,该指针始终指向栈的顶部,用于追踪哪些栈帧是活动的,哪些可以被安全回收。
简单理解:说白了就是某个作用域代码执行完毕后,自动回收作用域内所有的普通数据类型的垃圾。
堆回收
堆中变量存储的分类
V8引擎引入所谓的“代际假说”,即“大部分对象在内存中存在的时间很短”,所以把堆内存分为新生代和老生代两部分,新生代即对象一开始创建时存放的区域,一旦对象满足了一定的条件(也就是V8认为其会长时间存在于内存),那么它就会被移动(“晋升”)至老生代。
所以针对堆内存中的垃圾回收,也分为两种算法来分别处理新老生代。
新生代垃圾回收
采用Scavenge算法,简单来说就是Copy,也叫新生代互换。
结合上图:新生代空间等分成From空间与To空间,新声明的变量存放到新生代的From空间,直到From空间满了,这时候需要进行GC的垃圾回收。GC会根据代码分析出哪些obj不是垃圾,然后把非垃圾的obj对象直接copy到To空间,最后把From空间中的垃圾进行删除即可,至此存放对象的To空间变成From空间,原本的From空间变成To空间,从而开启新一轮的对象存储。
之所以新生代采用这种回收算法,可以从时空复杂度进行分析,首先我们会发现新生代的空间在同一时刻只有一半被利用,把空间进行了折半,目的就是为了对象转移时直接进行copy操作(肯定耗时非常低),可以理解为空间换时间。
并且新生代的总空间相较于老生代比较小,所以折半空间不会损失太多的内存空间,可以接受。
老生代垃圾回收
标记清除
在V8引擎早期,采用的是Mark-Sweep算法(标记清除),如下图:
注:上面图示的GC根节点并不是指浏览器的window
对象,但可以类比window
对象,我们可以通过window.xxx.xxx.xx
访问所有的变量,自然可以通过GC的根节点访问所有的内存对象,一旦GC断开了与某个对象的联系,那么这个对象就可以理解为垃圾。
标记清除算法的完整步骤:
- 从GC的根节点开始进行“广度扫描”(专属术语)并把非垃圾进行标记
- 清除未被标记的对象
标记整理
现阶段Mark-Compact算法(标记整理)已经成为主流,如下图:
标记整理算法步骤:
- 先进行广度扫描并标记非垃圾变量
- 整理非垃圾变量的位置,目的是获取更大的连续空间,如上图一到图二(整理的过程中会对一些垃圾进行覆盖,可以理解为先整理后清除的原因)
- 垃圾清除
也就是相较于标记清除算法多了一步:在垃圾清除之前进行内存空间的整理
标记算法优化
如上我们所描述的标记,都是“全停顿标记”,也就是说js主线程切换至GC垃圾回收线程时,在GC线程中会把所有的应该标记的变量都进行标记,然后进行大规模的清除。
这种标记算法的劣势就是时间开销大,导致GC线程占用的时间长,阻塞js主线程的运行。所以后期标记算法也进行了优化,衍生出来增量标记与三色标记法,这里就不详细记录了,总之优化的核心思想就是部分标记,从而减少GC线程对js线程运行时间的抢占。
plus
当然更早期的IE浏览器还使用过引用计数的堆回收算法,存在着不能解决循环引用问题等痛点。这里不再赘述。
到此这篇关于一文搞懂V8垃圾回收系统的文章就介绍到这了,更多相关V8垃圾回收系统内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!