V8的垃圾回收算法
JavaScript的对象在V8引擎的堆中创建,V8会自动回收不被引用的对象。采用这种方式,降低了内存管理的负担,但也造成了一些不便。
V8在执行垃圾回收的时候会阻塞JavaScript代码的执行,因此,堆内存的大小就需要进行限制,堆内存过大会导致回收算法执行时间过长。
从宏观上来看,V8的堆分为三个部分:年轻分代,年老分代,大对象空间。
(1)年轻分代
年轻分代采用了复制算法,堆空间一分为二,只有一半处于使用中,另外一半用于垃圾清理。年轻分代主要保存那些生命周期短暂的对象,例如函数中的局部变量。它们类似于C++中在栈上分配的对象,当函数返回,调用栈中的局部变量都会被析构掉。V8了解内存的使用情况,当发现内存空间不够,需要清理时,才进行回收。
具体步骤是,将还被引用的对象复制到另一半区域,然后释放当前一半的空间,把当前被释放的空间留作备用,两者角色互换。年轻分代类似于线程的栈空间,本身不会太大,占用它空间的对象类似于C++中的局部对象,生命周期非常短,因此,大部分都是需要被清理掉的,需要复制的对象极少。虽然牺牲了部分内存,但速度极快。
在C++程序中,当调用一个函数时,函数内部定义的局部对象会占用栈空间,栈空间是有限的,因此,函数的嵌套也是有限的。随着函数调用的结束,栈空间也被释放掉。而JavaScript代码,对象使用的空间是在年轻分代中分配,当要在堆中分配而内存不够时,由于新对象的挤压,会将超出生命期的垃圾对象清除出去。
(2)年老分代
年老分代的对象类似于C++中使用new操作符在堆中分配的对象。这类对象一般不会随着函数的退出而销毁,因此生命期较长。年老分代的大小远大于年轻分代,主要包含如下数据:从年轻分代中移动过来的对象,JIT之后产生的代码,全局对象。
年老分代中需要回收的对象比例极小,如果采用年轻分代一样的清理算法,会对导致很多不需要清理的对象被复制,所以,年老分代采用了不同的垃圾回收算法。
年老分代,采用了标记清除+标记整理算法,将垃圾回收分为两个过程。标记清除阶段,遍历堆中的所有对象,把有效的对象标记出来,之后清除垃圾对象。当执行完一次标记清除后,堆内存变得不连续,内存碎片的存在使得不能有效使用内存。在后续的执行中,当遇到没有一块碎片内存能够满足申请对象需要的内存空间时,将会触发V8执行标记整理算法。标记整理移动对象,紧缩V8的堆空间,将碎片的内存整理为大块内存。
实际上,V8执行这些算法的时候,并不是一次完成的,而是走走停停,因为垃圾回收会阻塞JavaScript代码的运行,所以采取交替运行的方式,有效的减少了垃圾回收给程序造成的最大停顿时间。
(3)大对象空间
大对象空间主要存放需要较大内存的对象,也包括数据和JIT生成的代码。垃圾回收不会移动大对象,这部分内存使用的特点是,整块分配,一次性整块回收。