前言
垃圾回收分为手动回收和自动回收两种策略,c/c++使用的就是手动回收,何时分配内存、何时销毁代码都是代码控制的。
js/java/python使用的是自动回收,本篇文章主要来讲讲js的垃圾回收机制。因为数据是存储在栈和堆两种内存空间中的,
所以接下来我们就来分别介绍“栈中的垃圾数据”和“堆中的垃圾数据”是如何回收。
调用栈中的数据回收
我们通过一段数据来分析:
function foo(){
var a = 1
var b = { name: "守夜人"}
function showName(){
var c = "小小大西瓜"
var d = { name: "张三"}
}
showName()
}
foo()
当代码执行到第六行时,调用栈从上到下分别是showName函数和foo函数的执行上下文,因为基本数据类型是分配在栈中的,引用类型分配到堆中,所以当函数的执行上下文被销毁后,栈中的数据就被回收了,那栈中的执行上下文是怎么样被销毁的呢?
-
当执行到showName函数的时候会有一个
记录当前执行状态的指针(称为ESP)指向栈中showName函数的执行上下文,表示当前正在执行showName函数 -
接着当showName函数执行完成之后,函数执⾏流程就进⼊了foo函数,那这时就需要销毁showName函数的执⾏上下⽂了。ESP这时候就帮上忙了,JavaScript会将ESP下移到foo函数的执⾏上下⽂,这个下移操作就是销毁showName函数执⾏上下⽂的过程。
堆中的数据回收
上述代码中虽然两个函数执行上下文被销毁了,基本类型的数据被回收了,但是两个引用类型的对象依然占用空间,接下来就说说堆中的数据是如何回收的。
v8会把堆分成新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间长的对象。新⽣区通常只⽀持1~8M的容量,
⽽⽼⽣区⽀持的容量就⼤很多了。
对于这两块区域,V8分别使⽤两个不同的垃圾回收器,以便更⾼效地实施垃圾回收。
-
主垃圾收集器:
老生代的垃圾回收 -
副垃圾收集器:
新生代的垃圾回收
垃圾回收器的⼯作流程
不论什么来垃圾回收器,都有一套共同的执行流程,也就是标记-清除-整理。
-
标记空间中活动对象和⾮活动对象。所谓活动对象就是还在使⽤的对象,⾮活动对象就是可以进⾏垃圾回收的对象。
-
回收⾮活动对象所占据的内存。进入环境的变量加上标记,离开环境的变量去掉标记,之后还有标记的就是要回收的对象。
-
做内存整理。因为频繁回收对象后,内存中就会存在大量不连续空间(称为内存碎片),但是这步受可选的,因为有的垃圾回收器不会产生内存碎片,比如“副垃圾选择器”。
副垃圾回收器
主要回收新生区的垃圾回收,使用的是scavenge算法,就是把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域。
-
在垃圾回收过程中,⾸先要对对象区域中的垃圾做标记;标记完成之后,就进⼊垃圾清理阶段。 副垃圾回收器会
把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来,所以这个复制过程,也就相当于完成了内存整理操作,复制后空闲区域就没有内存碎⽚了。 -
完成复制后,对象区域与空闲区域进⾏⻆⾊翻转,也就是
原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。这样就完成了垃圾对象的回收操作,同时这种⻆⾊翻转的操作还能让新⽣代中的这两块区域⽆限重复使⽤下去。 -
由于新⽣代中采⽤的Scavenge算法,所以每次执⾏清理操作时,都需要将存活的对象从对象区域复制到空闲区域。但复制操作需要时间成本,如果新⽣区空间设置得太⼤了,那么每次清理的时间就会过久,所以为了执⾏效率,⼀般新⽣区的空间会被设置得⽐较⼩。
-
因为新生区的空间不大,所以很容易被存货的对象装满整个区域,为了解决这个问题。js引入
对象晋升策略:经过两次垃圾回收依然还存活的对象,就会被移动到老生区中。
主垃圾收集器
主垃圾收集器主要负责老生区的垃圾回收,除了新生区晋升的对象,一些大的对象会直接分配到老生区中,因为老生区中的对象占用空间大,存活时间长。
老生区对象大,不适用scavenge算法,因为复制这些大的对象花费太多时间,导致回收执行效率不高,还会浪费一半的空间, 主垃圾回收器采用标记-清除
算法进行垃圾回收。
因为标记清除会产生内存碎片,所以产生了另外一种算法:标记-整理,标记过程是一样的,后续是让所有存活的对象都向一端移动,然后直接清除掉端边界的内存。
全停顿
因为js是单线程,一旦执行垃圾回收算法,都需要暂停正在执行的脚本,等垃圾收集完毕之后再恢复脚本执行,这就叫做全停顿。
为了降低老生代的垃圾回收而造成的卡顿,v8将标记过程分成一个个的子标记过程,同时让垃圾回收标记和js应用逻辑交替执行,直到标记完成,我们称这个算法为增量标记。
使用增量标记算法可以把一个完整的垃圾回收任务拆成很多小的任务,这些小的任务执行时间比较短,穿插在其他的js任务中间执行,就不会造成因为垃圾回收而感受到页面的卡顿了。